Compare commits

..

72 Commits

Author SHA1 Message Date
viktor d30e778bd4 fixed test
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m35s
2026-04-21 16:54:52 +02:00
viktor 73e867f75a fixed test to properly wait for goroutine completion 2026-04-21 16:54:28 +02:00
viktor 84b87d61f2 updated dependencies and go 2026-04-21 11:06:01 +02:00
Mikescher f62e7499ec v0.0.633
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m42s
2026-04-13 16:12:09 +02:00
Mikescher 1edc2712ed v0.0.632 Pubsub.PublishAsync
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m41s
2026-04-13 16:08:28 +02:00
Mikescher 63c28b4141 v0.0.631
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m0s
2026-03-14 15:14:38 +01:00
Mikescher b01e659bb4 v0.0.630
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m24s
2026-03-14 14:10:09 +01:00
Mikescher 0923fa7c09 v0.0.629 excelext
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2026-03-14 14:09:13 +01:00
Mikescher e19cb30713 v0.0.628
Build Docker and Deploy / Run goext test-suite (push) Successful in 4m37s
2026-03-14 01:13:37 +01:00
Mikescher 90dc6079d5 v0.0.627
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2026-03-14 01:13:07 +01:00
Mikescher 5b5c262994 v0.0.626 wsw
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m40s
2026-03-14 00:50:15 +01:00
Mikescher 44ec5e4804 v0.0.625
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m31s
2026-03-11 11:43:26 +01:00
TimoV 2054c04442 added test for StrRemoveControlCharacters func
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m39s
2026-03-10 19:58:04 +01:00
TimoV 373d28d405 added remove control characters func
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m37s
2026-03-10 19:54:28 +01:00
Mikescher 8aaf2cd257 v0.0.623
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m46s
2026-02-03 11:33:03 +01:00
Mikescher a373876d32 Remove redundant failure-mail step from pipeline (now handled by gitea itself)
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m40s
2026-01-23 10:16:30 +01:00
Mikescher 12324ba60d v0.0.622 fix tests
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m38s
2026-01-09 07:01:41 +01:00
Mikescher a032d09ea2 v0.0.621
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m34s
2026-01-08 23:04:46 +01:00
Mikescher 4bebc9ea38 v0.0.620
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2026-01-08 23:03:55 +01:00
Mikescher 580906ea08 v0.0.619 better handle error in ginext.JSON() with invalid data
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m26s
2026-01-08 22:58:50 +01:00
Mikescher 0f28e3d11b v0.0.618 remove deprecated param 'user_id' from scn.send()
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m46s
2025-12-06 22:20:02 +01:00
Mikescher 55f80432bb v0.0.617
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m18s
2025-12-06 11:54:36 +01:00
Mikescher d6daf0e285 v0.0.616
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2025-12-06 11:53:59 +01:00
Mikescher 1c2bc060da v0.0.615 MultiMutex
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m55s
2025-12-06 11:50:54 +01:00
Mikescher 34023dca4c v0.0.614 ArrCount
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m26s
2025-11-29 12:49:17 +01:00
Mikescher 69f2dd73c5 fix tests
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m23s
2025-11-06 09:54:28 +01:00
christopher dce6f634b1 v0.0.613 Add function to convert RFC3339Time To RFC3339NanoTime. Add function to shuffle array
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m37s
2025-11-05 14:48:04 +01:00
christopher 833f74427f Add ToNano method to convert RFC3339Time to RFC3339NanoTime 2025-11-05 14:45:47 +01:00
christopher 535a699584 Add ArrShuffle function to shuffle array elements 2025-11-05 14:45:38 +01:00
Mikescher b439b95f83 v0.0.612 fix sender-name in scn package
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m33s
2025-11-02 23:14:31 +01:00
Mikescher 995a82d90a Update googleapi/README.md
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m43s
2025-10-22 11:39:01 +02:00
Mikescher 0db10845ed v0.0.611 add dataext.MutexSet
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m45s
2025-10-15 09:55:14 +02:00
Mikescher 128ca25aa2 v0.0.610
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m17s
2025-10-04 00:29:57 +02:00
Mikescher f5d13ebe64 v0.0.609
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2025-10-04 00:29:09 +02:00
Mikescher 9730a91ad5 v0.0.608
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2025-10-04 00:27:41 +02:00
Mikescher 8c16e4d982 v0.0.607
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m17s
2025-10-04 00:24:13 +02:00
Mikescher 039a53a395 v0.0.606 add .DataMeta() to enums
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m18s
2025-10-04 00:08:32 +02:00
Mikescher 2cf571579b v0.0.605 do not panic in GoJSONRenderer
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m16s
2025-09-29 16:39:16 +02:00
Mikescher 9a537bb8c2 v0.0.604
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m16s
2025-09-20 15:21:15 +02:00
Mikescher 78ad103151 v0.0.603
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2025-09-20 15:19:09 +02:00
Mikescher c764a946ff v0.0.602 add listener to DelayedCombiningInvoker
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m18s
2025-09-20 15:13:02 +02:00
Mikescher ef59b1241f Fix tests
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m14s
2025-09-13 20:41:37 +02:00
Mikescher a70ab33559 v0.0.601 Add Wait and Update method to Atomic[T]
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m34s
2025-09-13 19:04:52 +02:00
Mikescher a58bb4b14b v0.0.600
Build Docker and Deploy / Run goext test-suite (push) Failing after 2m30s
2025-09-13 18:45:23 +02:00
Mikescher dc62bbe55f v0.0.599 implement dataext.broadcaster
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m37s
2025-09-13 18:42:17 +02:00
Mikescher b832d77d3e v0.0.598 prevent json marshalling of PassHash
Build Docker and Deploy / Run goext test-suite (push) Failing after 2m35s
2025-09-11 11:17:34 +02:00
Mikescher 38467cb4e7 v0.0.597 add update methods to SyncMap
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m47s
2025-09-04 14:25:06 +02:00
TimoV 68b06158b3 v0.0.596 force json map
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m42s
2025-08-26 15:55:57 +02:00
Mikescher 5f51173276 v0.0.595 fix zerlog channel for exerr [ZeroLogErrTraces] output and WRN errors
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m33s
2025-08-20 13:03:21 +02:00
Mikescher 1586314e3e v0.0.594 Add exerr OutputRaw(http.ResponseWriter) method
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m35s
2025-07-16 17:13:07 +02:00
Mikescher 254fe1556a v0.0.593 made PubSub more generic (namespace can be any comparable type)
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m54s
2025-07-16 12:50:36 +02:00
Mikescher 52e74b59f5 v0.0.592
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m30s
2025-07-16 12:46:18 +02:00
Mikescher 64f2cd7219 v0.0.591 implement namespaced PubSub Broker in dataext
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2025-07-16 12:44:55 +02:00
Mikescher a29aec8fb5 v0.0.590 more rfctime equal fixes for chris
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m34s
2025-07-15 14:14:22 +02:00
Mikescher 8ea9b3f79f v0.0.589 improve Equal method of rfctime structs - prevents panic in cmp library
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m48s
2025-07-15 13:46:36 +02:00
Mikescher a4b2a0589f v0.0.588
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m37s
2025-07-11 11:50:29 +02:00
Mikescher 4ef5f6059b v0.0.587
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m37s
2025-07-06 22:24:44 +02:00
Mikescher b23a444aa2 v0.0.586
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m33s
2025-07-04 13:56:53 +02:00
Mikescher 09932046f8 v0.0.585
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m36s
2025-07-04 11:46:00 +02:00
Mikescher 37e52595a2 v0.0.584
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m33s
2025-06-26 16:48:07 +02:00
Mikescher 95d7c90492 v0.0.583
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m31s
2025-06-25 10:59:23 +02:00
Mikescher 23a3235c7e v0.0.582
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m33s
2025-06-25 10:51:38 +02:00
Mikescher 506d276962 v0.0.581
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m39s
2025-06-25 10:28:54 +02:00
Mikescher 2a0cf84416 v0.0.580 Add IsZero() to generated ID types
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m33s
2025-06-16 08:40:07 +02:00
Mikescher 073aa84dd4 v0.0.579 fix StackSkip count on exerr zero-logger for Build()
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m29s
2025-06-13 16:53:47 +02:00
Mikescher a0dc9e92e4 v0.0.578
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m30s
2025-06-11 14:37:51 +02:00
Mikescher 98c591b019 v0.0.577
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m7s
2025-05-18 21:42:18 +02:00
Mikescher a93b93a3cd v0.0.576
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m26s
2025-05-18 14:26:24 +02:00
Mikescher 49bc52d63e v0.0.575 DelayedCombiningInvoker
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m31s
2025-05-11 19:17:05 +02:00
Mikescher 959020e3c0 v0.0.574 add syncMap.clear()
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m37s
2025-05-07 15:28:15 +02:00
TimoV 395e83acf6 panic bf url
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m36s
2025-05-06 19:17:49 +02:00
Mikescher 55ff89f179 v0.0.572 switch to git.blackforestbytes.com as module name
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2025-05-03 16:43:59 +02:00
189 changed files with 4310 additions and 1168 deletions
-13
View File
@@ -39,17 +39,4 @@ jobs:
- name: Run tests - name: Run tests
run: cd "${{ gitea.workspace }}" && make test 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"
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AgentMigrationStateService">
<option name="migrationStatus" value="COMPLETED" />
</component>
</project>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AskMigrationStateService">
<option name="migrationStatus" value="COMPLETED" />
</component>
</project>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Ask2AgentMigrationStateService">
<option name="migrationStatus" value="COMPLETED" />
</component>
</project>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="EditMigrationStateService">
<option name="migrationStatus" value="COMPLETED" />
</component>
</project>
+11
View File
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GoImports">
<option name="excludedPackages">
<array>
<option value="github.com/pkg/errors" />
<option value="golang.org/x/net/context" />
</array>
</option>
</component>
</project>
+5 -1
View File
@@ -5,7 +5,7 @@ A collection of general & useful library methods
This should not have any heavy dependencies (gin, mongo, etc) and add missing basic language features... This should not have any heavy dependencies (gin, mongo, etc) and add missing basic language features...
Potentially needs `export GOPRIVATE="gogs.mikescher.com"` Potentially needs `export GOPRIVATE="git.blackforestbytes.com"`
## Packages: ## Packages:
@@ -26,10 +26,14 @@ Potentially needs `export GOPRIVATE="gogs.mikescher.com"`
| cursortoken | Mike | MongoDB cursortoken implementation | | cursortoken | Mike | MongoDB cursortoken implementation |
| pagination | Mike | Pagination implementation | | pagination | Mike | Pagination implementation |
| | | | | | | |
| ginext | Mike | gin wrapper |
| wsw | Mike | websocket wrapper |
| | | |
| totpext | Mike | Implementation of TOTP (2-Factor-Auth) | | totpext | Mike | Implementation of TOTP (2-Factor-Auth) |
| termext | Mike | Utilities for terminals (mostly color output) | | termext | Mike | Utilities for terminals (mostly color output) |
| confext | Mike | Parses environment configuration into structs | | confext | Mike | Parses environment configuration into structs |
| cmdext | Mike | Runner for external commands/processes | | cmdext | Mike | Runner for external commands/processes |
| excelext | Mike | Build Excel files |
| | | | | | | |
| sq | Mike | Utility functions for sql based databases (primarily sqlite) | | sq | Mike | Utility functions for sql based databases (primarily sqlite) |
| tst | Mike | Utility functions for unit tests | | tst | Mike | Utility functions for unit tests |
+8 -7
View File
@@ -5,11 +5,11 @@ import (
_ "embed" _ "embed"
"errors" "errors"
"fmt" "fmt"
"git.blackforestbytes.com/BlackForestBytes/goext"
"git.blackforestbytes.com/BlackForestBytes/goext/cryptext"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
"git.blackforestbytes.com/BlackForestBytes/goext/rext"
"go/format" "go/format"
"gogs.mikescher.com/BlackForestBytes/goext"
"gogs.mikescher.com/BlackForestBytes/goext/cryptext"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/rext"
"io" "io"
"os" "os"
"path" "path"
@@ -64,16 +64,17 @@ func GenerateCharsetIDSpecs(sourceDir string, destFile string, opt CSIDGenOption
files = langext.ArrFilter(files, func(v os.DirEntry) bool { return !strings.HasSuffix(v.Name(), "_gen.go") }) files = langext.ArrFilter(files, func(v os.DirEntry) bool { return !strings.HasSuffix(v.Name(), "_gen.go") })
langext.SortBy(files, func(v os.DirEntry) string { return v.Name() }) langext.SortBy(files, func(v os.DirEntry) string { return v.Name() })
newChecksumStr := goext.GoextVersion var newChecksumStr strings.Builder
newChecksumStr.WriteString(goext.GoextVersion)
for _, f := range files { for _, f := range files {
content, err := os.ReadFile(path.Join(sourceDir, f.Name())) content, err := os.ReadFile(path.Join(sourceDir, f.Name()))
if err != nil { if err != nil {
return err return err
} }
newChecksumStr += "\n" + f.Name() + "\t" + cryptext.BytesSha256(content) newChecksumStr.WriteString("\n" + f.Name() + "\t" + cryptext.BytesSha256(content))
} }
newChecksum := cryptext.BytesSha256([]byte(newChecksumStr)) newChecksum := cryptext.BytesSha256([]byte(newChecksumStr.String()))
if newChecksum != oldChecksum { if newChecksum != oldChecksum {
fmt.Printf("[CSIDGenerate] 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)
+7 -3
View File
@@ -7,9 +7,9 @@ import "crypto/sha256"
import "fmt" import "fmt"
import "github.com/go-playground/validator/v10" import "github.com/go-playground/validator/v10"
import "github.com/rs/zerolog/log" import "github.com/rs/zerolog/log"
import "gogs.mikescher.com/BlackForestBytes/goext/exerr" import "git.blackforestbytes.com/BlackForestBytes/goext/exerr"
import "gogs.mikescher.com/BlackForestBytes/goext/langext" import "git.blackforestbytes.com/BlackForestBytes/goext/langext"
import "gogs.mikescher.com/BlackForestBytes/goext/rext" import "git.blackforestbytes.com/BlackForestBytes/goext/rext"
import "math/big" import "math/big"
import "reflect" import "reflect"
import "regexp" import "regexp"
@@ -183,6 +183,10 @@ func (id {{.Name}}) CheckString() string {
return getCheckString(prefix{{.Name}}, string(id)) return getCheckString(prefix{{.Name}}, string(id))
} }
func (id {{.Name}}) IsZero() bool {
return id == ""
}
func (id {{.Name}}) Regex() rext.Regex { func (id {{.Name}}) Regex() rext.Regex {
return regex{{.Name}} return regex{{.Name}}
} }
+3 -3
View File
@@ -3,9 +3,9 @@ package bfcodegen
import ( import (
_ "embed" _ "embed"
"fmt" "fmt"
"gogs.mikescher.com/BlackForestBytes/goext/cmdext" "git.blackforestbytes.com/BlackForestBytes/goext/cmdext"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "git.blackforestbytes.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/tst" "git.blackforestbytes.com/BlackForestBytes/goext/tst"
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"
+11 -10
View File
@@ -6,11 +6,11 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"git.blackforestbytes.com/BlackForestBytes/goext"
"git.blackforestbytes.com/BlackForestBytes/goext/cryptext"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
"git.blackforestbytes.com/BlackForestBytes/goext/rext"
"go/format" "go/format"
"gogs.mikescher.com/BlackForestBytes/goext"
"gogs.mikescher.com/BlackForestBytes/goext/cryptext"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/rext"
"io" "io"
"os" "os"
"path" "path"
@@ -46,7 +46,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 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<comm>.*))?.*$`)) 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_]*)"`)) var rexEnumChecksumConst = rext.W(regexp.MustCompile(`const ChecksumEnumGenerator = "(?P<cs>[A-Za-z0-9_]*)"`))
@@ -95,16 +95,17 @@ func _generateEnumSpecs(sourceDir string, destFile string, oldChecksum string, g
files = langext.ArrFilter(files, func(v os.DirEntry) bool { return !strings.HasSuffix(v.Name(), "_gen.go") }) files = langext.ArrFilter(files, func(v os.DirEntry) bool { return !strings.HasSuffix(v.Name(), "_gen.go") })
langext.SortBy(files, func(v os.DirEntry) string { return v.Name() }) langext.SortBy(files, func(v os.DirEntry) string { return v.Name() })
newChecksumStr := goext.GoextVersion var newChecksumStr strings.Builder
newChecksumStr.WriteString(goext.GoextVersion)
for _, f := range files { for _, f := range files {
content, err := os.ReadFile(path.Join(sourceDir, f.Name())) content, err := os.ReadFile(path.Join(sourceDir, f.Name()))
if err != nil { if err != nil {
return "", "", false, err return "", "", false, err
} }
newChecksumStr += "\n" + f.Name() + "\t" + cryptext.BytesSha256(content) newChecksumStr.WriteString("\n" + f.Name() + "\t" + cryptext.BytesSha256(content))
} }
newChecksum := cryptext.BytesSha256([]byte(newChecksumStr)) newChecksum := cryptext.BytesSha256([]byte(newChecksumStr.String()))
if newChecksum != oldChecksum { if newChecksum != oldChecksum {
fmt.Printf("[EnumGenerate] Checksum has changed ( %s -> %s ), will generate new file\n\n", oldChecksum, newChecksum) fmt.Printf("[EnumGenerate] Checksum has changed ( %s -> %s ), will generate new file\n\n", oldChecksum, newChecksum)
@@ -213,7 +214,7 @@ func processEnumFile(basedir string, fn string, debugOutput bool) ([]EnumDef, st
var descr *string = nil var descr *string = nil
var data *map[string]any = nil var data *map[string]any = nil
if comment != nil { if comment != nil {
comment = langext.Ptr(strings.TrimSpace(*comment)) comment = new(strings.TrimSpace(*comment))
if strings.HasPrefix(*comment, "{") { if strings.HasPrefix(*comment, "{") {
if v, ok := tryParseDataComment(*comment); ok { if v, ok := tryParseDataComment(*comment); ok {
data = &v data = &v
@@ -278,7 +279,7 @@ func tryParseDataComment(s string) (map[string]any, bool) {
rv := reflect.ValueOf(v) rv := reflect.ValueOf(v)
if rv.Kind() == reflect.Ptr && rv.IsNil() { if rv.Kind() == reflect.Pointer && rv.IsNil() {
continue continue
} }
if rv.Kind() == reflect.Bool { if rv.Kind() == reflect.Bool {
+24 -2
View File
@@ -2,8 +2,8 @@
package {{.PkgName}} package {{.PkgName}}
import "gogs.mikescher.com/BlackForestBytes/goext/langext" import "git.blackforestbytes.com/BlackForestBytes/goext/langext"
import "gogs.mikescher.com/BlackForestBytes/goext/enums" import "git.blackforestbytes.com/BlackForestBytes/goext/enums"
const ChecksumEnumGenerator = "{{.Checksum}}" // GoExtVersion: {{.GoextVersion}} const ChecksumEnumGenerator = "{{.Checksum}}" // GoExtVersion: {{.GoextVersion}}
@@ -117,6 +117,20 @@ func (e {{.EnumTypeName}}) DescriptionMeta() enums.EnumDescriptionMetaValue {
} }
{{end}} {{end}}
{{if $hasData}}
func (e {{.EnumTypeName}}) DataMeta() enums.EnumDataMetaValue {
return enums.EnumDataMetaValue{
VarName: e.VarName(),
Value: e,
{{if $hasDescr}} Description: langext.Ptr(e.Description()), {{else}} Description: nil, {{end}}
Data: map[string]any{
{{ range $datakey, $datatype := $enumdef | generalDataKeys }} "{{ $datakey }}": e.Data().{{ $datakey | godatakey }},
{{ end }}
},
}
}
{{end}}
func Parse{{.EnumTypeName}}(vv string) ({{.EnumTypeName}}, bool) { func Parse{{.EnumTypeName}}(vv string) ({{.EnumTypeName}}, bool) {
for _, ev := range __{{.EnumTypeName}}Values { for _, ev := range __{{.EnumTypeName}}Values {
if string(ev) == vv { if string(ev) == vv {
@@ -136,6 +150,14 @@ func {{.EnumTypeName}}ValuesMeta() []enums.EnumMetaValue {
} }
} }
{{if $hasData}}
func {{.EnumTypeName}}ValuesDataMeta() []enums.EnumDataMetaValue {
return []enums.EnumDataMetaValue{ {{range .Values}}
{{.VarName}}.DataMeta(), {{end}}
}
}
{{end}}
{{if $hasDescr}} {{if $hasDescr}}
func {{.EnumTypeName}}ValuesDescriptionMeta() []enums.EnumDescriptionMetaValue { func {{.EnumTypeName}}ValuesDescriptionMeta() []enums.EnumDescriptionMetaValue {
return []enums.EnumDescriptionMetaValue{ {{range .Values}} return []enums.EnumDescriptionMetaValue{ {{range .Values}}
+3 -3
View File
@@ -3,9 +3,9 @@ package bfcodegen
import ( import (
_ "embed" _ "embed"
"fmt" "fmt"
"gogs.mikescher.com/BlackForestBytes/goext/cmdext" "git.blackforestbytes.com/BlackForestBytes/goext/cmdext"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "git.blackforestbytes.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/tst" "git.blackforestbytes.com/BlackForestBytes/goext/tst"
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"
+8 -7
View File
@@ -5,11 +5,11 @@ import (
_ "embed" _ "embed"
"errors" "errors"
"fmt" "fmt"
"git.blackforestbytes.com/BlackForestBytes/goext"
"git.blackforestbytes.com/BlackForestBytes/goext/cryptext"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
"git.blackforestbytes.com/BlackForestBytes/goext/rext"
"go/format" "go/format"
"gogs.mikescher.com/BlackForestBytes/goext"
"gogs.mikescher.com/BlackForestBytes/goext/cryptext"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/rext"
"io" "io"
"os" "os"
"path" "path"
@@ -63,16 +63,17 @@ func GenerateIDSpecs(sourceDir string, destFile string, opt IDGenOptions) error
files = langext.ArrFilter(files, func(v os.DirEntry) bool { return !strings.HasSuffix(v.Name(), "_gen.go") }) files = langext.ArrFilter(files, func(v os.DirEntry) bool { return !strings.HasSuffix(v.Name(), "_gen.go") })
langext.SortBy(files, func(v os.DirEntry) string { return v.Name() }) langext.SortBy(files, func(v os.DirEntry) string { return v.Name() })
newChecksumStr := goext.GoextVersion var newChecksumStr strings.Builder
newChecksumStr.WriteString(goext.GoextVersion)
for _, f := range files { for _, f := range files {
content, err := os.ReadFile(path.Join(sourceDir, f.Name())) content, err := os.ReadFile(path.Join(sourceDir, f.Name()))
if err != nil { if err != nil {
return err return err
} }
newChecksumStr += "\n" + f.Name() + "\t" + cryptext.BytesSha256(content) newChecksumStr.WriteString("\n" + f.Name() + "\t" + cryptext.BytesSha256(content))
} }
newChecksum := cryptext.BytesSha256([]byte(newChecksumStr)) newChecksum := cryptext.BytesSha256([]byte(newChecksumStr.String()))
if newChecksum != oldChecksum { if newChecksum != oldChecksum {
fmt.Printf("[IDGenerate] Checksum has changed ( %s -> %s ), will generate new file\n\n", oldChecksum, newChecksum) fmt.Printf("[IDGenerate] Checksum has changed ( %s -> %s ), will generate new file\n\n", oldChecksum, newChecksum)
+5 -1
View File
@@ -5,7 +5,7 @@ package {{.PkgName}}
import "go.mongodb.org/mongo-driver/bson" import "go.mongodb.org/mongo-driver/bson"
import "go.mongodb.org/mongo-driver/bson/bsontype" import "go.mongodb.org/mongo-driver/bson/bsontype"
import "go.mongodb.org/mongo-driver/bson/primitive" import "go.mongodb.org/mongo-driver/bson/primitive"
import "gogs.mikescher.com/BlackForestBytes/goext/exerr" import "git.blackforestbytes.com/BlackForestBytes/goext/exerr"
const ChecksumIDGenerator = "{{.Checksum}}" // GoExtVersion: {{.GoextVersion}} const ChecksumIDGenerator = "{{.Checksum}}" // GoExtVersion: {{.GoextVersion}}
@@ -45,6 +45,10 @@ func (i {{.Name}}) AsAnyPtr() *{{$.AnyDef.Name}} {
} }
{{end}} {{end}}
func (i {{.Name}}) IsZero() bool {
return i == ""
}
func New{{.Name}}() {{.Name}} { func New{{.Name}}() {{.Name}} {
return {{.Name}}(primitive.NewObjectID().Hex()) return {{.Name}}(primitive.NewObjectID().Hex())
} }
+3 -3
View File
@@ -3,9 +3,9 @@ package bfcodegen
import ( import (
_ "embed" _ "embed"
"fmt" "fmt"
"gogs.mikescher.com/BlackForestBytes/goext/cmdext" "git.blackforestbytes.com/BlackForestBytes/goext/cmdext"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "git.blackforestbytes.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/tst" "git.blackforestbytes.com/BlackForestBytes/goext/tst"
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"
+3 -3
View File
@@ -2,7 +2,7 @@ package cmdext
import ( import (
"fmt" "fmt"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "git.blackforestbytes.com/BlackForestBytes/goext/langext"
"time" "time"
) )
@@ -61,12 +61,12 @@ func (r *CommandRunner) Envs(env []string) *CommandRunner {
} }
func (r *CommandRunner) EnsureExitcode(arg ...int) *CommandRunner { func (r *CommandRunner) EnsureExitcode(arg ...int) *CommandRunner {
r.enforceExitCodes = langext.Ptr(langext.ForceArray(arg)) r.enforceExitCodes = new(langext.ForceArray(arg))
return r return r
} }
func (r *CommandRunner) FailOnExitCode() *CommandRunner { func (r *CommandRunner) FailOnExitCode() *CommandRunner {
r.enforceExitCodes = langext.Ptr([]int{0}) r.enforceExitCodes = new([]int{0})
return r return r
} }
+5 -5
View File
@@ -2,9 +2,9 @@ package cmdext
import ( import (
"errors" "errors"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "git.blackforestbytes.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/mathext" "git.blackforestbytes.com/BlackForestBytes/goext/mathext"
"gogs.mikescher.com/BlackForestBytes/goext/syncext" "git.blackforestbytes.com/BlackForestBytes/goext/syncext"
"os/exec" "os/exec"
"time" "time"
) )
@@ -37,7 +37,7 @@ func run(opt CommandRunner) (CommandResult, error) {
} }
preader := pipeReader{ preader := pipeReader{
lineBufferSize: langext.Ptr(128 * 1024 * 1024), // 128MB max size of a single line, is hopefully enough.... lineBufferSize: new(128 * 1024 * 1024), // 128MB max size of a single line, is hopefully enough....
stdout: stdoutPipe, stdout: stdoutPipe,
stderr: stderrPipe, stderr: stderrPipe,
} }
@@ -66,7 +66,7 @@ func run(opt CommandRunner) (CommandResult, error) {
if opt.enforceNoStderr { if opt.enforceNoStderr {
listener = append(listener, genericCommandListener{ listener = append(listener, genericCommandListener{
_readRawStderr: langext.Ptr(func(v []byte) { _readRawStderr: new(func(v []byte) {
if len(v) > 0 { if len(v) > 0 {
stderrFailChan <- true stderrFailChan <- true
} }
+13 -16
View File
@@ -2,8 +2,9 @@ package cmdext
import ( import (
"bufio" "bufio"
"gogs.mikescher.com/BlackForestBytes/goext/syncext" "git.blackforestbytes.com/BlackForestBytes/goext/syncext"
"io" "io"
"strings"
"sync" "sync"
) )
@@ -29,14 +30,14 @@ func (pr *pipeReader) Read(listener []CommandListener) (string, string, string,
wg.Add(1) wg.Add(1)
stdoutBufferReader, stdoutBufferWriter := io.Pipe() stdoutBufferReader, stdoutBufferWriter := io.Pipe()
stdout := "" var stdout strings.Builder
go func() { go func() {
buf := make([]byte, 128) buf := make([]byte, 128)
for { for {
n, err := pr.stdout.Read(buf) n, err := pr.stdout.Read(buf)
if n > 0 { if n > 0 {
txt := string(buf[:n]) txt := string(buf[:n])
stdout += txt stdout.WriteString(txt)
_, _ = stdoutBufferWriter.Write(buf[:n]) _, _ = stdoutBufferWriter.Write(buf[:n])
for _, lstr := range listener { for _, lstr := range listener {
lstr.ReadRawStdout(buf[:n]) lstr.ReadRawStdout(buf[:n])
@@ -58,7 +59,7 @@ func (pr *pipeReader) Read(listener []CommandListener) (string, string, string,
wg.Add(1) wg.Add(1)
stderrBufferReader, stderrBufferWriter := io.Pipe() stderrBufferReader, stderrBufferWriter := io.Pipe()
stderr := "" var stderr strings.Builder
go func() { go func() {
buf := make([]byte, 128) buf := make([]byte, 128)
for { for {
@@ -66,7 +67,7 @@ func (pr *pipeReader) Read(listener []CommandListener) (string, string, string,
if n > 0 { if n > 0 {
txt := string(buf[:n]) txt := string(buf[:n])
stderr += txt stderr.WriteString(txt)
_, _ = stderrBufferWriter.Write(buf[:n]) _, _ = stderrBufferWriter.Write(buf[:n])
for _, lstr := range listener { for _, lstr := range listener {
lstr.ReadRawStderr(buf[:n]) lstr.ReadRawStderr(buf[:n])
@@ -88,8 +89,7 @@ func (pr *pipeReader) Read(listener []CommandListener) (string, string, string,
// [3] collect stdout line-by-line // [3] collect stdout line-by-line
wg.Add(1) wg.Go(func() {
go func() {
scanner := bufio.NewScanner(stdoutBufferReader) scanner := bufio.NewScanner(stdoutBufferReader)
if pr.lineBufferSize != nil { if pr.lineBufferSize != nil {
scanner.Buffer([]byte{}, *pr.lineBufferSize) scanner.Buffer([]byte{}, *pr.lineBufferSize)
@@ -105,13 +105,11 @@ func (pr *pipeReader) Read(listener []CommandListener) (string, string, string,
errch <- err errch <- err
} }
combch <- combevt{"", true} combch <- combevt{"", true}
wg.Done() })
}()
// [4] collect stderr line-by-line // [4] collect stderr line-by-line
wg.Add(1) wg.Go(func() {
go func() {
scanner := bufio.NewScanner(stderrBufferReader) scanner := bufio.NewScanner(stderrBufferReader)
if pr.lineBufferSize != nil { if pr.lineBufferSize != nil {
scanner.Buffer([]byte{}, *pr.lineBufferSize) scanner.Buffer([]byte{}, *pr.lineBufferSize)
@@ -127,13 +125,12 @@ func (pr *pipeReader) Read(listener []CommandListener) (string, string, string,
errch <- err errch <- err
} }
combch <- combevt{"", true} combch <- combevt{"", true}
wg.Done() })
}()
// [5] combine stdcombined // [5] combine stdcombined
wg.Add(1) wg.Add(1)
stdcombined := "" var stdcombined strings.Builder
go func() { go func() {
stopctr := 0 stopctr := 0
for stopctr < 2 { for stopctr < 2 {
@@ -141,7 +138,7 @@ func (pr *pipeReader) Read(listener []CommandListener) (string, string, string,
if vvv.stop { if vvv.stop {
stopctr++ stopctr++
} else { } else {
stdcombined += vvv.line + "\n" // this comes from bufio.Scanner and has no newlines... stdcombined.WriteString(vvv.line + "\n") // this comes from bufio.Scanner and has no newlines...
} }
} }
wg.Done() wg.Done()
@@ -154,5 +151,5 @@ func (pr *pipeReader) Read(listener []CommandListener) (string, string, string,
return "", "", "", err return "", "", "", err
} }
return stdout, stderr, stdcombined, nil return stdout.String(), stderr.String(), stdcombined.String(), nil
} }
+10 -10
View File
@@ -3,7 +3,7 @@ package confext
import ( import (
"errors" "errors"
"fmt" "fmt"
"gogs.mikescher.com/BlackForestBytes/goext/timeext" "git.blackforestbytes.com/BlackForestBytes/goext/timeext"
"math/bits" "math/bits"
"os" "os"
"reflect" "reflect"
@@ -101,11 +101,11 @@ func processEnvOverrides(rval reflect.Value, delim string, prefix string) error
} }
func parseEnvToValue(envval string, fullEnvKey string, rvtype reflect.Type) (reflect.Value, error) { func parseEnvToValue(envval string, fullEnvKey string, rvtype reflect.Type) (reflect.Value, error) {
if rvtype == reflect.TypeOf("") { if rvtype == reflect.TypeFor[string]() {
return reflect.ValueOf(envval), nil return reflect.ValueOf(envval), nil
} else if rvtype == reflect.TypeOf(int(0)) { } else if rvtype == reflect.TypeFor[int]() {
envint, err := strconv.ParseInt(envval, 10, bits.UintSize) envint, err := strconv.ParseInt(envval, 10, bits.UintSize)
if err != nil { if err != nil {
@@ -114,7 +114,7 @@ func parseEnvToValue(envval string, fullEnvKey string, rvtype reflect.Type) (ref
return reflect.ValueOf(int(envint)), nil return reflect.ValueOf(int(envint)), nil
} else if rvtype == reflect.TypeOf(int64(0)) { } else if rvtype == reflect.TypeFor[int64]() {
envint, err := strconv.ParseInt(envval, 10, 64) envint, err := strconv.ParseInt(envval, 10, 64)
if err != nil { if err != nil {
@@ -123,7 +123,7 @@ func parseEnvToValue(envval string, fullEnvKey string, rvtype reflect.Type) (ref
return reflect.ValueOf(int64(envint)), nil return reflect.ValueOf(int64(envint)), nil
} else if rvtype == reflect.TypeOf(int32(0)) { } else if rvtype == reflect.TypeFor[int32]() {
envint, err := strconv.ParseInt(envval, 10, 32) envint, err := strconv.ParseInt(envval, 10, 32)
if err != nil { if err != nil {
@@ -132,7 +132,7 @@ func parseEnvToValue(envval string, fullEnvKey string, rvtype reflect.Type) (ref
return reflect.ValueOf(int32(envint)), nil return reflect.ValueOf(int32(envint)), nil
} else if rvtype == reflect.TypeOf(int8(0)) { } else if rvtype == reflect.TypeFor[int8]() {
envint, err := strconv.ParseInt(envval, 10, 8) envint, err := strconv.ParseInt(envval, 10, 8)
if err != nil { if err != nil {
@@ -141,7 +141,7 @@ func parseEnvToValue(envval string, fullEnvKey string, rvtype reflect.Type) (ref
return reflect.ValueOf(int8(envint)), nil return reflect.ValueOf(int8(envint)), nil
} else if rvtype == reflect.TypeOf(time.Duration(0)) { } else if rvtype == reflect.TypeFor[time.Duration]() {
dur, err := timeext.ParseDurationShortString(envval) dur, err := timeext.ParseDurationShortString(envval)
if err != nil { if err != nil {
@@ -159,7 +159,7 @@ func parseEnvToValue(envval string, fullEnvKey string, rvtype reflect.Type) (ref
return reflect.ValueOf(tim), nil return reflect.ValueOf(tim), nil
} else if rvtype.ConvertibleTo(reflect.TypeOf(int(0))) { } else if rvtype.ConvertibleTo(reflect.TypeFor[int]()) {
envint, err := strconv.ParseInt(envval, 10, 8) envint, err := strconv.ParseInt(envval, 10, 8)
if err != nil { if err != nil {
@@ -170,7 +170,7 @@ func parseEnvToValue(envval string, fullEnvKey string, rvtype reflect.Type) (ref
return envcvl, nil return envcvl, nil
} else if rvtype.ConvertibleTo(reflect.TypeOf(false)) { } else if rvtype.ConvertibleTo(reflect.TypeFor[bool]()) {
if strings.TrimSpace(strings.ToLower(envval)) == "true" { if strings.TrimSpace(strings.ToLower(envval)) == "true" {
return reflect.ValueOf(true).Convert(rvtype), nil return reflect.ValueOf(true).Convert(rvtype), nil
@@ -184,7 +184,7 @@ func parseEnvToValue(envval string, fullEnvKey string, rvtype reflect.Type) (ref
return reflect.Value{}, errors.New(fmt.Sprintf("Failed to parse env-config variable '%s' to <%s, ,bool> (value := '%s')", rvtype.Name(), fullEnvKey, envval)) return reflect.Value{}, errors.New(fmt.Sprintf("Failed to parse env-config variable '%s' to <%s, ,bool> (value := '%s')", rvtype.Name(), fullEnvKey, envval))
} }
} else if rvtype.ConvertibleTo(reflect.TypeOf("")) { } else if rvtype.ConvertibleTo(reflect.TypeFor[string]()) {
envcvl := reflect.ValueOf(envval).Convert(rvtype) envcvl := reflect.ValueOf(envval).Convert(rvtype)
return envcvl, nil return envcvl, nil
+2 -2
View File
@@ -1,8 +1,8 @@
package confext package confext
import ( import (
"gogs.mikescher.com/BlackForestBytes/goext/timeext" "git.blackforestbytes.com/BlackForestBytes/goext/timeext"
"gogs.mikescher.com/BlackForestBytes/goext/tst" "git.blackforestbytes.com/BlackForestBytes/goext/tst"
"testing" "testing"
"time" "time"
) )
+1 -1
View File
@@ -2,7 +2,7 @@ package cryptext
import ( import (
"fmt" "fmt"
"gogs.mikescher.com/BlackForestBytes/goext/tst" "git.blackforestbytes.com/BlackForestBytes/goext/tst"
"testing" "testing"
) )
+1 -1
View File
@@ -1,7 +1,7 @@
package cryptext package cryptext
import ( import (
"gogs.mikescher.com/BlackForestBytes/goext/tst" "git.blackforestbytes.com/BlackForestBytes/goext/tst"
"testing" "testing"
) )
+12 -3
View File
@@ -6,13 +6,15 @@ import (
"crypto/sha512" "crypto/sha512"
"encoding/base64" "encoding/base64"
"encoding/hex" "encoding/hex"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/totpext"
"golang.org/x/crypto/bcrypt"
"strconv" "strconv"
"strings" "strings"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
"git.blackforestbytes.com/BlackForestBytes/goext/totpext"
"golang.org/x/crypto/bcrypt"
) )
const LatestPassHashVersion = 5 const LatestPassHashVersion = 5
@@ -317,6 +319,13 @@ func (ph PassHash) String() string {
return string(ph) return string(ph)
} }
func (ph PassHash) MarshalJSON() ([]byte, error) {
if ph == "" {
return json.Marshal("")
}
return json.Marshal("*****")
}
func HashPassword(plainpass string, totpSecret []byte) (PassHash, error) { func HashPassword(plainpass string, totpSecret []byte) (PassHash, error) {
return HashPasswordV5(plainpass, totpSecret) return HashPasswordV5(plainpass, totpSecret)
} }
+7 -8
View File
@@ -1,9 +1,8 @@
package cryptext package cryptext
import ( import (
"gogs.mikescher.com/BlackForestBytes/goext/langext" "git.blackforestbytes.com/BlackForestBytes/goext/totpext"
"gogs.mikescher.com/BlackForestBytes/goext/totpext" "git.blackforestbytes.com/BlackForestBytes/goext/tst"
"gogs.mikescher.com/BlackForestBytes/goext/tst"
"testing" "testing"
) )
@@ -32,7 +31,7 @@ func TestPassHashTOTP(t *testing.T) {
tst.AssertFalse(t, ph.Verify("test123", nil)) tst.AssertFalse(t, ph.Verify("test123", nil))
tst.AssertFalse(t, ph.Verify("test124", nil)) tst.AssertFalse(t, ph.Verify("test124", nil))
tst.AssertTrue(t, ph.Verify("test123", langext.Ptr(totpext.TOTP(sec)))) tst.AssertTrue(t, ph.Verify("test123", new(totpext.TOTP(sec))))
tst.AssertFalse(t, ph.Verify("test124", nil)) tst.AssertFalse(t, ph.Verify("test124", nil))
} }
@@ -141,7 +140,7 @@ func TestPassHashUpgrade_V3_TOTP(t *testing.T) {
tst.AssertFalse(t, ph.Verify("test123", nil)) tst.AssertFalse(t, ph.Verify("test123", nil))
tst.AssertFalse(t, ph.Verify("test124", nil)) tst.AssertFalse(t, ph.Verify("test124", nil))
tst.AssertTrue(t, ph.Verify("test123", langext.Ptr(totpext.TOTP(sec)))) tst.AssertTrue(t, ph.Verify("test123", new(totpext.TOTP(sec))))
tst.AssertFalse(t, ph.Verify("test124", nil)) tst.AssertFalse(t, ph.Verify("test124", nil))
ph, err = ph.Upgrade("test123") ph, err = ph.Upgrade("test123")
@@ -153,7 +152,7 @@ func TestPassHashUpgrade_V3_TOTP(t *testing.T) {
tst.AssertFalse(t, ph.Verify("test123", nil)) tst.AssertFalse(t, ph.Verify("test123", nil))
tst.AssertFalse(t, ph.Verify("test124", nil)) tst.AssertFalse(t, ph.Verify("test124", nil))
tst.AssertTrue(t, ph.Verify("test123", langext.Ptr(totpext.TOTP(sec)))) tst.AssertTrue(t, ph.Verify("test123", new(totpext.TOTP(sec))))
tst.AssertFalse(t, ph.Verify("test124", nil)) tst.AssertFalse(t, ph.Verify("test124", nil))
} }
@@ -193,7 +192,7 @@ func TestPassHashUpgrade_V4_TOTP(t *testing.T) {
tst.AssertFalse(t, ph.Verify("test123", nil)) tst.AssertFalse(t, ph.Verify("test123", nil))
tst.AssertFalse(t, ph.Verify("test124", nil)) tst.AssertFalse(t, ph.Verify("test124", nil))
tst.AssertTrue(t, ph.Verify("test123", langext.Ptr(totpext.TOTP(sec)))) tst.AssertTrue(t, ph.Verify("test123", new(totpext.TOTP(sec))))
tst.AssertFalse(t, ph.Verify("test124", nil)) tst.AssertFalse(t, ph.Verify("test124", nil))
ph, err = ph.Upgrade("test123") ph, err = ph.Upgrade("test123")
@@ -205,6 +204,6 @@ func TestPassHashUpgrade_V4_TOTP(t *testing.T) {
tst.AssertFalse(t, ph.Verify("test123", nil)) tst.AssertFalse(t, ph.Verify("test123", nil))
tst.AssertFalse(t, ph.Verify("test124", nil)) tst.AssertFalse(t, ph.Verify("test124", nil))
tst.AssertTrue(t, ph.Verify("test123", langext.Ptr(totpext.TOTP(sec)))) tst.AssertTrue(t, ph.Verify("test123", new(totpext.TOTP(sec))))
tst.AssertFalse(t, ph.Verify("test124", nil)) tst.AssertFalse(t, ph.Verify("test124", nil))
} }
+3 -3
View File
@@ -7,21 +7,21 @@ import (
) )
func TestPronouncablePasswordExt(t *testing.T) { func TestPronouncablePasswordExt(t *testing.T) {
for i := 0; i < 20; i++ { for i := range 20 {
pw, entropy := PronouncablePasswordExt(rand.New(rand.NewSource(int64(i))), 16) pw, entropy := PronouncablePasswordExt(rand.New(rand.NewSource(int64(i))), 16)
fmt.Printf("[%.2f] => %s\n", entropy, pw) fmt.Printf("[%.2f] => %s\n", entropy, pw)
} }
} }
func TestPronouncablePasswordSeeded(t *testing.T) { func TestPronouncablePasswordSeeded(t *testing.T) {
for i := 0; i < 20; i++ { for i := range 20 {
pw := PronouncablePasswordSeeded(int64(i), 8) pw := PronouncablePasswordSeeded(int64(i), 8)
fmt.Printf("%s\n", pw) fmt.Printf("%s\n", pw)
} }
} }
func TestPronouncablePassword(t *testing.T) { func TestPronouncablePassword(t *testing.T) {
for i := 0; i < 20; i++ { for i := range 20 {
pw := PronouncablePassword(i + 1) pw := PronouncablePassword(i + 1)
fmt.Printf("%s\n", pw) fmt.Printf("%s\n", pw)
} }
+1 -1
View File
@@ -3,7 +3,7 @@ package cursortoken
import ( import (
"encoding/base32" "encoding/base32"
"encoding/json" "encoding/json"
"gogs.mikescher.com/BlackForestBytes/goext/exerr" "git.blackforestbytes.com/BlackForestBytes/goext/exerr"
"strconv" "strconv"
"strings" "strings"
"time" "time"
+230
View File
@@ -0,0 +1,230 @@
package dataext
import (
"context"
"iter"
"sync"
"time"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
"git.blackforestbytes.com/BlackForestBytes/goext/syncext"
"github.com/rs/xid"
)
// Broadcaster is a simple Broadcaster channel
// This is a simpler interface over Broadcaster - which does not have distinct namespaces
type Broadcaster[TData any] struct {
masterLock *sync.Mutex
subscriptions []*broadcastSubscription[TData]
}
type BroadcastSubscription interface {
Unsubscribe()
}
type broadcastSubscription[TData any] struct {
ID string
parent *Broadcaster[TData]
subLock *sync.Mutex
Func func(TData)
Chan chan TData
UnsubChan chan bool
}
func (p *broadcastSubscription[TData]) Unsubscribe() {
p.parent.unsubscribe(p)
}
func NewBroadcaster[TData any](capacity int) *Broadcaster[TData] {
return &Broadcaster[TData]{
masterLock: &sync.Mutex{},
subscriptions: make([]*broadcastSubscription[TData], 0, capacity),
}
}
func (bb *Broadcaster[TData]) SubscriberCount() int {
bb.masterLock.Lock()
defer bb.masterLock.Unlock()
return len(bb.subscriptions)
}
// Publish sends `data` to all subscriber
// But unbuffered - if one is currently not listening, we skip (the actualReceiver < subscriber)
func (bb *Broadcaster[TData]) Publish(data TData) (subscriber int, actualReceiver int) {
bb.masterLock.Lock()
subs := langext.ArrCopy(bb.subscriptions)
bb.masterLock.Unlock()
subscriber = len(subs)
actualReceiver = 0
for _, sub := range subs {
func() {
sub.subLock.Lock()
defer sub.subLock.Unlock()
if sub.Func != nil {
go func() { sub.Func(data) }()
actualReceiver++
} else if sub.Chan != nil {
msgSent := syncext.WriteNonBlocking(sub.Chan, data)
if msgSent {
actualReceiver++
}
}
}()
}
return subscriber, actualReceiver
}
// PublishWithContext sends `data` to all subscriber
// buffered - if one is currently not listening, we wait (but error out when the context runs out)
func (bb *Broadcaster[TData]) PublishWithContext(ctx context.Context, data TData) (subscriber int, actualReceiver int, err error) {
bb.masterLock.Lock()
subs := langext.ArrCopy(bb.subscriptions)
bb.masterLock.Unlock()
subscriber = len(subs)
actualReceiver = 0
for _, sub := range subs {
err := func() error {
sub.subLock.Lock()
defer sub.subLock.Unlock()
if err := ctx.Err(); err != nil {
return err
}
if sub.Func != nil {
go func() { sub.Func(data) }()
actualReceiver++
} else if sub.Chan != nil {
err := syncext.WriteChannelWithContext(ctx, sub.Chan, data)
if err != nil {
return err
}
actualReceiver++
}
return nil
}()
if err != nil {
return subscriber, actualReceiver, err
}
}
return subscriber, actualReceiver, nil
}
// PublishWithTimeout sends `data` to all subscriber
// buffered - if one is currently not listening, we wait (but wait at most `timeout` - if the timeout is exceeded then actualReceiver < subscriber)
func (bb *Broadcaster[TData]) PublishWithTimeout(data TData, timeout time.Duration) (subscriber int, actualReceiver int) {
bb.masterLock.Lock()
subs := langext.ArrCopy(bb.subscriptions)
bb.masterLock.Unlock()
subscriber = len(subs)
actualReceiver = 0
for _, sub := range subs {
func() {
sub.subLock.Lock()
defer sub.subLock.Unlock()
if sub.Func != nil {
go func() { sub.Func(data) }()
actualReceiver++
} else if sub.Chan != nil {
ok := syncext.WriteChannelWithTimeout(sub.Chan, data, timeout)
if ok {
actualReceiver++
}
}
}()
}
return subscriber, actualReceiver
}
func (bb *Broadcaster[TData]) SubscribeByCallback(fn func(TData)) BroadcastSubscription {
bb.masterLock.Lock()
defer bb.masterLock.Unlock()
sub := &broadcastSubscription[TData]{ID: xid.New().String(), parent: bb, subLock: &sync.Mutex{}, Func: fn, UnsubChan: nil}
bb.subscriptions = append(bb.subscriptions, sub)
return sub
}
func (bb *Broadcaster[TData]) SubscribeByChan(chanBufferSize int) (chan TData, BroadcastSubscription) {
bb.masterLock.Lock()
defer bb.masterLock.Unlock()
msgCh := make(chan TData, chanBufferSize)
sub := &broadcastSubscription[TData]{ID: xid.New().String(), parent: bb, subLock: &sync.Mutex{}, Chan: msgCh, UnsubChan: nil}
bb.subscriptions = append(bb.subscriptions, sub)
return msgCh, sub
}
func (bb *Broadcaster[TData]) SubscribeByIter(chanBufferSize int) (iter.Seq[TData], BroadcastSubscription) {
bb.masterLock.Lock()
defer bb.masterLock.Unlock()
msgCh := make(chan TData, chanBufferSize)
unsubChan := make(chan bool, 8)
sub := &broadcastSubscription[TData]{ID: xid.New().String(), parent: bb, subLock: &sync.Mutex{}, Chan: msgCh, UnsubChan: unsubChan}
bb.subscriptions = append(bb.subscriptions, sub)
iterFun := func(yield func(TData) bool) {
for {
select {
case msg := <-msgCh:
if !yield(msg) {
sub.Unsubscribe()
return
}
case <-sub.UnsubChan:
sub.Unsubscribe()
return
}
}
}
return iterFun, sub
}
func (bb *Broadcaster[TData]) unsubscribe(p *broadcastSubscription[TData]) {
bb.masterLock.Lock()
defer bb.masterLock.Unlock()
p.subLock.Lock()
defer p.subLock.Unlock()
if p.Chan != nil {
close(p.Chan)
p.Chan = nil
}
if p.UnsubChan != nil {
syncext.WriteNonBlocking(p.UnsubChan, true)
close(p.UnsubChan)
p.UnsubChan = nil
}
bb.subscriptions = langext.ArrFilter(bb.subscriptions, func(v *broadcastSubscription[TData]) bool {
return v.ID != p.ID
})
}
+342
View File
@@ -0,0 +1,342 @@
package dataext
import (
"context"
"sync"
"testing"
"time"
)
func TestNewBroadcast(t *testing.T) {
bb := NewBroadcaster[string](10)
if bb == nil {
t.Fatal("NewBroadcaster returned nil")
}
if bb.masterLock == nil {
t.Fatal("masterLock is nil")
}
if bb.subscriptions == nil {
t.Fatal("subscriptions is nil")
}
}
func TestBroadcast_SubscribeByCallback(t *testing.T) {
bb := NewBroadcaster[string](10)
var received string
var wg sync.WaitGroup
wg.Add(1)
callback := func(msg string) {
received = msg
wg.Done()
}
sub := bb.SubscribeByCallback(callback)
defer sub.Unsubscribe()
// Publish a message
subs, receivers := bb.Publish("hello")
if subs != 1 {
t.Fatalf("Expected 1 subscriber, got %d", subs)
}
if receivers != 1 {
t.Fatalf("Expected 1 receiver, got %d", receivers)
}
// Wait for the callback to be executed
wg.Wait()
if received != "hello" {
t.Fatalf("Expected to receive 'hello', got '%s'", received)
}
}
func TestBroadcast_SubscribeByChan(t *testing.T) {
bb := NewBroadcaster[string](10)
ch, sub := bb.SubscribeByChan(1)
defer sub.Unsubscribe()
// Publish a message
subs, receivers := bb.Publish("hello")
if subs != 1 {
t.Fatalf("Expected 1 subscriber, got %d", subs)
}
if receivers != 1 {
t.Fatalf("Expected 1 receiver, got %d", receivers)
}
// Read from the channel with a timeout to avoid blocking
select {
case msg := <-ch:
if msg != "hello" {
t.Fatalf("Expected to receive 'hello', got '%s'", msg)
}
case <-time.After(time.Second):
t.Fatal("Timed out waiting for message")
}
}
func TestBroadcast_SubscribeByIter(t *testing.T) {
bb := NewBroadcaster[string](10)
iterSeq, sub := bb.SubscribeByIter(1)
defer sub.Unsubscribe()
// Channel to communicate when message is received
done := make(chan bool)
goroutineDone := make(chan struct{})
received := false
// Start a goroutine to use the iterator
go func() {
defer close(goroutineDone)
for msg := range iterSeq {
if msg == "hello" {
received = true
done <- true
return // Stop iteration — triggers Unsubscribe via yield returning false
}
}
}()
// Give time for the iterator to start
time.Sleep(100 * time.Millisecond)
// Publish a message
bb.Publish("hello")
// Wait for the message to be received or timeout
select {
case <-done:
if !received {
t.Fatal("Message was received but not 'hello'")
}
case <-time.After(time.Second):
t.Fatal("Timed out waiting for message")
}
// Wait for the goroutine to fully exit so Unsubscribe (triggered by the
// iterator cleanup when yield returns false) has completed.
select {
case <-goroutineDone:
case <-time.After(time.Second):
t.Fatal("Timed out waiting for goroutine to finish")
}
subCount := bb.SubscriberCount()
if subCount != 0 {
t.Fatalf("Expected 0 receivers, got %d", subCount)
}
}
func TestBroadcast_Publish(t *testing.T) {
bb := NewBroadcaster[string](10)
// Test publishing with no subscribers
subs, receivers := bb.Publish("hello")
if subs != 0 {
t.Fatalf("Expected 0 subscribers, got %d", subs)
}
if receivers != 0 {
t.Fatalf("Expected 0 receivers, got %d", receivers)
}
// Add a subscriber
ch, sub := bb.SubscribeByChan(1)
defer sub.Unsubscribe()
// Publish a message
subs, receivers = bb.Publish("hello")
if subs != 1 {
t.Fatalf("Expected 1 subscriber, got %d", subs)
}
if receivers != 1 {
t.Fatalf("Expected 1 receiver, got %d", receivers)
}
// Verify the message was received
select {
case msg := <-ch:
if msg != "hello" {
t.Fatalf("Expected to receive 'hello', got '%s'", msg)
}
case <-time.After(time.Second):
t.Fatal("Timed out waiting for message")
}
// Test non-blocking behavior with a full channel
// First fill the channel
bb.Publish("fill")
// Now publish again - this should not block but may skip the receiver
subs, receivers = bb.Publish("overflow")
if subs != 1 {
t.Fatalf("Expected 1 subscriber, got %d", subs)
}
_ = receivers // may be 0 if channel is full
// Drain the channel
<-ch
}
func TestBroadcast_PublishWithTimeout(t *testing.T) {
bb := NewBroadcaster[string](10)
// Add a subscriber with a channel
ch, sub := bb.SubscribeByChan(1)
defer sub.Unsubscribe()
// Publish with a timeout
subs, receivers := bb.PublishWithTimeout("hello", 100*time.Millisecond)
if subs != 1 {
t.Fatalf("Expected 1 subscriber, got %d", subs)
}
if receivers != 1 {
t.Fatalf("Expected 1 receiver, got %d", receivers)
}
// Verify the message was received
select {
case msg := <-ch:
if msg != "hello" {
t.Fatalf("Expected to receive 'hello', got '%s'", msg)
}
case <-time.After(time.Second):
t.Fatal("Timed out waiting for message")
}
// Fill the channel
bb.Publish("fill")
// Test timeout behavior with a full channel
start := time.Now()
subs, receivers = bb.PublishWithTimeout("timeout-test", 50*time.Millisecond)
elapsed := time.Since(start)
if subs != 1 {
t.Fatalf("Expected 1 subscriber, got %d", subs)
}
// The receiver count should be 0 if the timeout occurred
if elapsed < 50*time.Millisecond {
t.Fatalf("Expected to wait at least 50ms, only waited %v", elapsed)
}
// Drain the channel
<-ch
}
func TestBroadcast_PublishWithContext(t *testing.T) {
bb := NewBroadcaster[string](10)
// Add a subscriber with a channel
ch, sub := bb.SubscribeByChan(1)
defer sub.Unsubscribe()
// Create a context
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
// Publish with context
subs, receivers, err := bb.PublishWithContext(ctx, "hello")
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if subs != 1 {
t.Fatalf("Expected 1 subscriber, got %d", subs)
}
if receivers != 1 {
t.Fatalf("Expected 1 receiver, got %d", receivers)
}
// Verify the message was received
select {
case msg := <-ch:
if msg != "hello" {
t.Fatalf("Expected to receive 'hello', got '%s'", msg)
}
case <-time.After(time.Second):
t.Fatal("Timed out waiting for message")
}
// Fill the channel
bb.Publish("fill")
// Test context cancellation with a full channel
ctx, cancel = context.WithCancel(context.Background())
// Cancel the context after a short delay
go func() {
time.Sleep(50 * time.Millisecond)
cancel()
}()
start := time.Now()
subs, receivers, err = bb.PublishWithContext(ctx, "context-test")
elapsed := time.Since(start)
if subs != 1 {
t.Fatalf("Expected 1 subscriber, got %d", subs)
}
// Should get a context canceled error
if err == nil {
t.Fatal("Expected context canceled error, got nil")
}
if elapsed < 50*time.Millisecond {
t.Fatalf("Expected to wait at least 50ms, only waited %v", elapsed)
}
// Drain the channel
<-ch
}
func TestBroadcast_Unsubscribe(t *testing.T) {
bb := NewBroadcaster[string](10)
// Add a subscriber
ch, sub := bb.SubscribeByChan(1)
// Publish a message
subs, receivers := bb.Publish("hello")
if subs != 1 {
t.Fatalf("Expected 1 subscriber, got %d", subs)
}
if receivers != 1 {
t.Fatalf("Expected 1 receiver, got %d", receivers)
}
// Verify the message was received
select {
case msg := <-ch:
if msg != "hello" {
t.Fatalf("Expected to receive 'hello', got '%s'", msg)
}
case <-time.After(time.Second):
t.Fatal("Timed out waiting for message")
}
// Unsubscribe
sub.Unsubscribe()
// Publish again
subs, receivers = bb.Publish("after-unsub")
if subs != 0 {
t.Fatalf("Expected 0 subscribers after unsubscribe, got %d", subs)
}
if receivers != 0 {
t.Fatalf("Expected 0 receivers after unsubscribe, got %d", receivers)
}
// Check that the subscriber count is 0
if bb.SubscriberCount() != 0 {
t.Fatalf("Expected SubscriberCount() == 0, got %d", bb.SubscriberCount())
}
}
+199
View File
@@ -0,0 +1,199 @@
package dataext
import (
"context"
"sync"
"time"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
"git.blackforestbytes.com/BlackForestBytes/goext/syncext"
)
// DelayedCombiningInvoker is a utility to combine multiple consecutive requests into a single execution
//
// Requests are made with Request(), and consecutive requests are combined during the `delay` period.
//
// Can be used, e.g., for search-controls, where we want to init the search when teh user stops typing
// Or generally to queue an execution once a burst of requests is over.
type DelayedCombiningInvoker struct {
syncLock sync.Mutex
triggerChan chan bool
cancelChan chan bool
execNowChan chan bool
action func()
delay time.Duration
maxDelay time.Duration
executorRunning *syncext.AtomicBool
pendingRequests *syncext.Atomic[int]
lastRequestTime time.Time
initialRequestTime time.Time
onExecutionStart []func(immediately bool) // listener ( actual execution of action starts )
onExecutionDone []func() // listener ( actual execution of action finished )
onRequest []func(pending int, initial bool) // listener ( a request came in, waiting for execution )
}
func NewDelayedCombiningInvoker(action func(), delay time.Duration, maxDelay time.Duration) *DelayedCombiningInvoker {
return &DelayedCombiningInvoker{
action: action,
delay: delay,
maxDelay: maxDelay,
executorRunning: syncext.NewAtomicBool(false),
pendingRequests: syncext.NewAtomic[int](0),
triggerChan: make(chan bool),
cancelChan: make(chan bool, 1),
execNowChan: make(chan bool, 1),
lastRequestTime: time.Now(),
initialRequestTime: time.Now(),
onExecutionStart: make([]func(bool), 0),
onExecutionDone: make([]func(), 0),
onRequest: make([]func(int, bool), 0),
}
}
func (d *DelayedCombiningInvoker) Request() {
now := time.Now()
d.syncLock.Lock()
defer d.syncLock.Unlock()
if d.executorRunning.Get() {
d.lastRequestTime = now
d.pendingRequests.Update(func(v int) int { return v + 1 })
for _, fn := range d.onRequest {
_ = langext.RunPanicSafe(func() { fn(d.pendingRequests.Get(), true) })
}
d.triggerChan <- true
} else {
d.initialRequestTime = now
d.lastRequestTime = now
d.executorRunning.Set(true)
d.pendingRequests.Set(1)
syncext.ReadNonBlocking(d.triggerChan) // clear the channel
syncext.ReadNonBlocking(d.cancelChan) // clear the channel
syncext.ReadNonBlocking(d.execNowChan) // clear the channel
for _, fn := range d.onRequest {
_ = langext.RunPanicSafe(func() { fn(d.pendingRequests.Get(), false) })
}
go d.run()
}
}
func (d *DelayedCombiningInvoker) run() {
needsExecutorRunningCleanup := true
defer func() {
if needsExecutorRunningCleanup {
d.syncLock.Lock()
d.executorRunning.Set(false)
d.syncLock.Unlock()
}
}()
for {
d.syncLock.Lock()
timeOut := max(min(d.maxDelay-time.Since(d.initialRequestTime), d.delay-time.Since(d.lastRequestTime)), 0)
d.syncLock.Unlock()
immediately := false
select {
case <-d.execNowChan:
// run immediately
immediately = true
break
case <-d.triggerChan:
// external trigger - needs to re-evaluate
break
case <-d.cancelChan:
// cancel
return
case <-time.After(timeOut):
// time elapsed - check for execution
break
}
d.syncLock.Lock()
execute := immediately || time.Since(d.lastRequestTime) >= d.delay || time.Since(d.initialRequestTime) >= d.maxDelay
if !execute {
d.syncLock.Unlock()
continue
}
d.pendingRequests.Set(0)
for _, fn := range d.onExecutionStart {
_ = langext.RunPanicSafe(func() { fn(immediately) })
}
// =================================================
_ = langext.RunPanicSafe(d.action)
// =================================================
d.executorRunning.Set(false) // ensure HasPendingRequests returns fals ein onExecutionDone listener
needsExecutorRunningCleanup = false
for _, fn := range d.onExecutionDone {
_ = langext.RunPanicSafe(fn)
}
d.syncLock.Unlock()
return
}
}
func (d *DelayedCombiningInvoker) CancelPendingRequests() {
d.syncLock.Lock()
defer d.syncLock.Unlock()
syncext.WriteNonBlocking(d.cancelChan, true)
}
func (d *DelayedCombiningInvoker) HasPendingRequests() bool {
return d.executorRunning.Get()
}
func (d *DelayedCombiningInvoker) CountPendingRequests() int {
return d.pendingRequests.Get()
}
func (d *DelayedCombiningInvoker) ExecuteNow() bool {
d.syncLock.Lock()
defer d.syncLock.Unlock()
if d.executorRunning.Get() {
syncext.WriteNonBlocking(d.execNowChan, true)
return true
} else {
return false
}
}
func (d *DelayedCombiningInvoker) WaitForCompletion(ctx context.Context) error {
return d.executorRunning.WaitWithContext(ctx, false)
}
func (d *DelayedCombiningInvoker) RegisterOnExecutionStart(fn func(immediately bool)) {
d.syncLock.Lock()
defer d.syncLock.Unlock()
d.onExecutionStart = append(d.onExecutionStart, fn)
}
func (d *DelayedCombiningInvoker) RegisterOnExecutionDone(fn func()) {
d.syncLock.Lock()
defer d.syncLock.Unlock()
d.onExecutionDone = append(d.onExecutionDone, fn)
}
func (d *DelayedCombiningInvoker) RegisterOnRequest(fn func(pending int, initial bool)) {
d.syncLock.Lock()
defer d.syncLock.Unlock()
d.onRequest = append(d.onRequest, fn)
}
+1 -1
View File
@@ -1,7 +1,7 @@
package dataext package dataext
import ( import (
"gogs.mikescher.com/BlackForestBytes/goext/langext" "git.blackforestbytes.com/BlackForestBytes/goext/langext"
"math/rand" "math/rand"
"strconv" "strconv"
"testing" "testing"
+1 -1
View File
@@ -14,7 +14,7 @@ func ObjectMerge[T1 any, T2 any](base T1, override T2) T1 {
fieldBase := reflBase.Field(i) fieldBase := reflBase.Field(i)
fieldOvrd := reflOvrd.Field(i) fieldOvrd := reflOvrd.Field(i)
if fieldBase.Kind() != reflect.Ptr || fieldOvrd.Kind() != reflect.Ptr { if fieldBase.Kind() != reflect.Pointer || fieldOvrd.Kind() != reflect.Pointer {
continue continue
} }
+5 -6
View File
@@ -1,8 +1,7 @@
package dataext package dataext
import ( import (
"gogs.mikescher.com/BlackForestBytes/goext/langext" "git.blackforestbytes.com/BlackForestBytes/goext/tst"
"gogs.mikescher.com/BlackForestBytes/goext/tst"
"testing" "testing"
) )
@@ -26,17 +25,17 @@ func TestObjectMerge(t *testing.T) {
valueA := A{ valueA := A{
Field1: nil, Field1: nil,
Field2: langext.Ptr("99"), Field2: new("99"),
Field3: langext.Ptr(12.2), Field3: new(12.2),
Field4: nil, Field4: nil,
OnlyA: 1, OnlyA: 1,
DiffType: 2, DiffType: 2,
} }
valueB := B{ valueB := B{
Field1: langext.Ptr(12), Field1: new(12),
Field2: nil, Field2: nil,
Field3: langext.Ptr(13.2), Field3: new(13.2),
Field4: nil, Field4: nil,
OnlyB: 1, OnlyB: 1,
DiffType: "X", DiffType: "X",
+105
View File
@@ -0,0 +1,105 @@
package dataext
import (
"context"
"sync"
"time"
)
// MultiMutex is a simple map[key -> mutex]
type MultiMutex[TKey comparable] struct {
mutextMap *SyncMap[TKey, *CASMutex]
}
func NewMultiMutex[TKey comparable]() *MultiMutex[TKey] {
return &MultiMutex[TKey]{
mutextMap: NewSyncMap[TKey, *CASMutex](),
}
}
// TryLockWithContext attempts to acquire the lock, blocking until resources
// are available or ctx is done (timeout or cancellation).
func (mm *MultiMutex[TKey]) TryLockWithContext(ctx context.Context, key TKey) bool {
lck := mm.mutextMap.GetAndSetIfNotContainsFunc(key, NewCASMutex)
return lck.TryLockWithContext(ctx)
}
// Lock acquires the lock.
// If it is currently held by others, Lock will wait until it has a chance to acquire it.
func (mm *MultiMutex[TKey]) Lock(key TKey) {
lck := mm.mutextMap.GetAndSetIfNotContainsFunc(key, NewCASMutex)
lck.Lock()
}
// TryLock attempts to acquire the lock without blocking.
// Return false if someone is holding it now.
func (mm *MultiMutex[TKey]) TryLock(key TKey) bool {
lck := mm.mutextMap.GetAndSetIfNotContainsFunc(key, NewCASMutex)
return lck.TryLock()
}
// TryLockWithTimeout attempts to acquire the lock within a period of time.
// Return false if spending time is more than duration and no chance to acquire it.
func (mm *MultiMutex[TKey]) TryLockWithTimeout(key TKey, duration time.Duration) bool {
lck := mm.mutextMap.GetAndSetIfNotContainsFunc(key, NewCASMutex)
return lck.TryLockWithTimeout(duration)
}
// Unlock releases the lock.
func (mm *MultiMutex[TKey]) Unlock(key TKey) {
lck := mm.mutextMap.GetAndSetIfNotContainsFunc(key, NewCASMutex)
lck.Unlock()
}
// RTryLockWithContext attempts to acquire the read lock, blocking until resources
// are available or ctx is done (timeout or cancellation).
func (mm *MultiMutex[TKey]) RTryLockWithContext(ctx context.Context, key TKey) bool {
lck := mm.mutextMap.GetAndSetIfNotContainsFunc(key, NewCASMutex)
return lck.RTryLockWithContext(ctx)
}
// RLock acquires the read lock.
// If it is currently held by others writing, RLock will wait until it has a chance to acquire it.
func (mm *MultiMutex[TKey]) RLock(key TKey) {
lck := mm.mutextMap.GetAndSetIfNotContainsFunc(key, NewCASMutex)
lck.RLock()
}
// RTryLock attempts to acquire the read lock without blocking.
// Return false if someone is writing it now.
func (mm *MultiMutex[TKey]) RTryLock(key TKey) bool {
lck := mm.mutextMap.GetAndSetIfNotContainsFunc(key, NewCASMutex)
return lck.RTryLock()
}
// RTryLockWithTimeout attempts to acquire the read lock within a period of time.
// Return false if spending time is more than duration and no chance to acquire it.
func (mm *MultiMutex[TKey]) RTryLockWithTimeout(duration time.Duration, key TKey) bool {
lck := mm.mutextMap.GetAndSetIfNotContainsFunc(key, NewCASMutex)
return lck.RTryLockWithTimeout(duration)
}
// RUnlock releases the read lock.
func (mm *MultiMutex[TKey]) RUnlock(key TKey) {
lck := mm.mutextMap.GetAndSetIfNotContainsFunc(key, NewCASMutex)
lck.RUnlock()
}
// RLocker returns a Locker interface that implements the Lock and Unlock methods
// by calling CASMutex.RLock and CASMutex.RUnlock.
func (mm *MultiMutex[TKey]) RLocker(key TKey) sync.Locker {
lck := mm.mutextMap.GetAndSetIfNotContainsFunc(key, NewCASMutex)
return lck.RLocker()
}
// Get returns a Locker interface
func (mm *MultiMutex[TKey]) Get(key TKey) sync.Locker {
lck := mm.mutextMap.GetAndSetIfNotContainsFunc(key, NewCASMutex)
return lck
}
// GetCAS returns the underlying CASMutex
func (mm *MultiMutex[TKey]) GetCAS(key TKey) *CASMutex {
lck := mm.mutextMap.GetAndSetIfNotContainsFunc(key, NewCASMutex)
return lck
}
+53
View File
@@ -0,0 +1,53 @@
package dataext
import "sync"
type MutexSet[T comparable] struct {
master sync.RWMutex
locks map[T]*sync.RWMutex
}
func NewMutexSet[T comparable]() *MutexSet[T] {
return &MutexSet[T]{
master: sync.RWMutex{},
locks: make(map[T]*sync.RWMutex),
}
}
func (ms *MutexSet[T]) get(key T) *sync.RWMutex {
ms.master.RLock()
if v, ok := ms.locks[key]; ok {
ms.master.RUnlock()
return v
}
ms.master.RUnlock()
// ---------
ms.master.Lock()
defer ms.master.Unlock()
if v, ok := ms.locks[key]; ok {
return v
}
m := &sync.RWMutex{}
ms.locks[key] = m
return m
}
func (ms *MutexSet[T]) Lock(key T) {
ms.get(key).Lock()
}
func (ms *MutexSet[T]) Unlock(key T) {
ms.get(key).Unlock()
}
func (ms *MutexSet[T]) RLock(key T) {
ms.get(key).RLock()
}
func (ms *MutexSet[T]) RUnlock(key T) {
ms.get(key).RUnlock()
}
+1 -1
View File
@@ -3,7 +3,7 @@ package dataext
import ( import (
"encoding/json" "encoding/json"
"errors" "errors"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "git.blackforestbytes.com/BlackForestBytes/goext/langext"
) )
type JsonOpt[T any] struct { type JsonOpt[T any] struct {
+267
View File
@@ -0,0 +1,267 @@
package dataext
import (
"context"
"iter"
"sync"
"time"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
"git.blackforestbytes.com/BlackForestBytes/goext/syncext"
"github.com/rs/xid"
)
// PubSub is a simple Pub/Sub Broker
// Clients can subscribe to a namespace and receive published messages on this namespace
// Messages are broadcast to all subscribers
type PubSub[TNamespace comparable, TData any] struct {
masterLock *sync.Mutex
subscriptions map[TNamespace][]*pubSubSubscription[TNamespace, TData]
}
type PubSubSubscription interface {
Unsubscribe()
}
type pubSubSubscription[TNamespace comparable, TData any] struct {
ID string
parent *PubSub[TNamespace, TData]
namespace TNamespace
subLock *sync.Mutex
Func func(TData)
Chan chan TData
UnsubChan chan bool
}
func (p *pubSubSubscription[TNamespace, TData]) Unsubscribe() {
p.parent.unsubscribe(p)
}
func NewPubSub[TNamespace comparable, TData any](capacity int) *PubSub[TNamespace, TData] {
return &PubSub[TNamespace, TData]{
masterLock: &sync.Mutex{},
subscriptions: make(map[TNamespace][]*pubSubSubscription[TNamespace, TData], capacity),
}
}
func (ps *PubSub[TNamespace, TData]) Namespaces() []TNamespace {
ps.masterLock.Lock()
defer ps.masterLock.Unlock()
return langext.MapKeyArr(ps.subscriptions)
}
func (ps *PubSub[TNamespace, TData]) SubscriberCount(ns TNamespace) int {
ps.masterLock.Lock()
defer ps.masterLock.Unlock()
return len(ps.subscriptions[ns])
}
// Publish sends `data` to all subscriber
// But unbuffered - if one is currently not listening, we skip (the actualReceiver < subscriber)
func (ps *PubSub[TNamespace, TData]) Publish(ns TNamespace, data TData) (subscriber int, actualReceiver int) {
ps.masterLock.Lock()
subs := langext.ArrCopy(ps.subscriptions[ns])
ps.masterLock.Unlock()
subscriber = len(subs)
actualReceiver = 0
for _, sub := range subs {
func() {
sub.subLock.Lock()
defer sub.subLock.Unlock()
if sub.Func != nil {
go func() { sub.Func(data) }()
actualReceiver++
} else if sub.Chan != nil {
msgSent := syncext.WriteNonBlocking(sub.Chan, data)
if msgSent {
actualReceiver++
}
}
}()
}
return subscriber, actualReceiver
}
// PublishWithContext sends `data` to all subscriber
// buffered - if one is currently not listening, we wait (but error out when the context runs out)
func (ps *PubSub[TNamespace, TData]) PublishWithContext(ctx context.Context, ns TNamespace, data TData) (subscriber int, actualReceiver int, err error) {
ps.masterLock.Lock()
subs := langext.ArrCopy(ps.subscriptions[ns])
ps.masterLock.Unlock()
subscriber = len(subs)
actualReceiver = 0
for _, sub := range subs {
err := func() error {
sub.subLock.Lock()
defer sub.subLock.Unlock()
if err := ctx.Err(); err != nil {
return err
}
if sub.Func != nil {
go func() { sub.Func(data) }()
actualReceiver++
} else if sub.Chan != nil {
err := syncext.WriteChannelWithContext(ctx, sub.Chan, data)
if err != nil {
return err
}
actualReceiver++
}
return nil
}()
if err != nil {
return subscriber, actualReceiver, err
}
}
return subscriber, actualReceiver, nil
}
// PublishWithTimeout sends `data` to all subscriber
// buffered - if one is currently not listening, we wait (but wait at most `timeout` - if the timeout is exceeded then actualReceiver < subscriber)
func (ps *PubSub[TNamespace, TData]) PublishWithTimeout(ns TNamespace, data TData, timeout time.Duration) (subscriber int, actualReceiver int) {
ps.masterLock.Lock()
subs := langext.ArrCopy(ps.subscriptions[ns])
ps.masterLock.Unlock()
subscriber = len(subs)
actualReceiver = 0
for _, sub := range subs {
func() {
sub.subLock.Lock()
defer sub.subLock.Unlock()
if sub.Func != nil {
go func() { sub.Func(data) }()
actualReceiver++
} else if sub.Chan != nil {
ok := syncext.WriteChannelWithTimeout(sub.Chan, data, timeout)
if ok {
actualReceiver++
}
}
}()
}
return subscriber, actualReceiver
}
// PublishAsync sends `data` to all subscriber
// does not wait for subscriber (this method returns immediately), waits at most {timeout} seconds on channels (async)
func (ps *PubSub[TNamespace, TData]) PublishAsync(ns TNamespace, data TData, timeout time.Duration) (subscriber int) {
ps.masterLock.Lock()
subs := langext.ArrCopy(ps.subscriptions[ns])
ps.masterLock.Unlock()
subscriber = len(subs)
for _, sub := range subs {
go func() {
sub.subLock.Lock()
defer sub.subLock.Unlock()
if sub.Func != nil {
go func() { sub.Func(data) }()
} else if sub.Chan != nil {
_ = syncext.WriteChannelWithTimeout(sub.Chan, data, timeout)
}
}()
}
return subscriber
}
func (ps *PubSub[TNamespace, TData]) SubscribeByCallback(ns TNamespace, fn func(TData)) PubSubSubscription {
ps.masterLock.Lock()
defer ps.masterLock.Unlock()
sub := &pubSubSubscription[TNamespace, TData]{ID: xid.New().String(), namespace: ns, parent: ps, subLock: &sync.Mutex{}, Func: fn, UnsubChan: nil}
ps.subscriptions[ns] = append(ps.subscriptions[ns], sub)
return sub
}
func (ps *PubSub[TNamespace, TData]) SubscribeByChan(ns TNamespace, chanBufferSize int) (chan TData, PubSubSubscription) {
ps.masterLock.Lock()
defer ps.masterLock.Unlock()
msgCh := make(chan TData, chanBufferSize)
sub := &pubSubSubscription[TNamespace, TData]{ID: xid.New().String(), namespace: ns, parent: ps, subLock: &sync.Mutex{}, Chan: msgCh, UnsubChan: nil}
ps.subscriptions[ns] = append(ps.subscriptions[ns], sub)
return msgCh, sub
}
func (ps *PubSub[TNamespace, TData]) SubscribeByIter(ns TNamespace, chanBufferSize int) (iter.Seq[TData], PubSubSubscription) {
ps.masterLock.Lock()
defer ps.masterLock.Unlock()
msgCh := make(chan TData, chanBufferSize)
unsubChan := make(chan bool, 8)
sub := &pubSubSubscription[TNamespace, TData]{ID: xid.New().String(), namespace: ns, parent: ps, subLock: &sync.Mutex{}, Chan: msgCh, UnsubChan: unsubChan}
ps.subscriptions[ns] = append(ps.subscriptions[ns], sub)
iterFun := func(yield func(TData) bool) {
for {
select {
case msg := <-msgCh:
if !yield(msg) {
sub.Unsubscribe()
return
}
case <-sub.UnsubChan:
sub.Unsubscribe()
return
}
}
}
return iterFun, sub
}
func (ps *PubSub[TNamespace, TData]) unsubscribe(p *pubSubSubscription[TNamespace, TData]) {
ps.masterLock.Lock()
defer ps.masterLock.Unlock()
p.subLock.Lock()
defer p.subLock.Unlock()
if p.Chan != nil {
close(p.Chan)
p.Chan = nil
}
if p.UnsubChan != nil {
syncext.WriteNonBlocking(p.UnsubChan, true)
close(p.UnsubChan)
p.UnsubChan = nil
}
ps.subscriptions[p.namespace] = langext.ArrFilter(ps.subscriptions[p.namespace], func(v *pubSubSubscription[TNamespace, TData]) bool {
return v.ID != p.ID
})
if len(ps.subscriptions[p.namespace]) == 0 {
delete(ps.subscriptions, p.namespace)
}
}
+438
View File
@@ -0,0 +1,438 @@
package dataext
import (
"context"
"sync"
"testing"
"time"
)
func TestNewPubSub(t *testing.T) {
ps := NewPubSub[string, string](10)
if ps == nil {
t.Fatal("NewPubSub returned nil")
}
if ps.masterLock == nil {
t.Fatal("masterLock is nil")
}
if ps.subscriptions == nil {
t.Fatal("subscriptions is nil")
}
}
func TestPubSub_Namespaces(t *testing.T) {
ps := NewPubSub[string, string](10)
// Initially no namespaces
namespaces := ps.Namespaces()
if len(namespaces) != 0 {
t.Fatalf("Expected 0 namespaces, got %d", len(namespaces))
}
// Add a subscription to create a namespace
_, sub1 := ps.SubscribeByChan("test-ns1", 1)
defer sub1.Unsubscribe()
// Add another subscription to a different namespace
_, sub2 := ps.SubscribeByChan("test-ns2", 1)
defer sub2.Unsubscribe()
// Check namespaces
namespaces = ps.Namespaces()
if len(namespaces) != 2 {
t.Fatalf("Expected 2 namespaces, got %d", len(namespaces))
}
// Check if namespaces contain the expected values
found1, found2 := false, false
for _, ns := range namespaces {
if ns == "test-ns1" {
found1 = true
}
if ns == "test-ns2" {
found2 = true
}
}
if !found1 || !found2 {
t.Fatalf("Expected to find both namespaces, found ns1: %v, ns2: %v", found1, found2)
}
}
func TestPubSub_SubscribeByCallback(t *testing.T) {
ps := NewPubSub[string, string](10)
var received string
var wg sync.WaitGroup
wg.Add(1)
callback := func(msg string) {
received = msg
wg.Done()
}
sub := ps.SubscribeByCallback("test-ns", callback)
defer sub.Unsubscribe()
// Publish a message
subs, receivers := ps.Publish("test-ns", "hello")
if subs != 1 {
t.Fatalf("Expected 1 subscriber, got %d", subs)
}
if receivers != 1 {
t.Fatalf("Expected 1 receiver, got %d", receivers)
}
// Wait for the callback to be executed
wg.Wait()
if received != "hello" {
t.Fatalf("Expected to receive 'hello', got '%s'", received)
}
}
func TestPubSub_SubscribeByChan(t *testing.T) {
ps := NewPubSub[string, string](10)
ch, sub := ps.SubscribeByChan("test-ns", 1)
defer sub.Unsubscribe()
// Publish a message
subs, receivers := ps.Publish("test-ns", "hello")
if subs != 1 {
t.Fatalf("Expected 1 subscriber, got %d", subs)
}
if receivers != 1 {
t.Fatalf("Expected 1 receiver, got %d", receivers)
}
// Read from the channel with a timeout to avoid blocking
select {
case msg := <-ch:
if msg != "hello" {
t.Fatalf("Expected to receive 'hello', got '%s'", msg)
}
case <-time.After(time.Second):
t.Fatal("Timed out waiting for message")
}
}
func TestPubSub_SubscribeByIter(t *testing.T) {
ps := NewPubSub[string, string](10)
iterSeq, sub := ps.SubscribeByIter("test-ns", 1)
defer sub.Unsubscribe()
// Channel to communicate when message is received
done := make(chan bool)
goroutineDone := make(chan struct{})
received := false
// Start a goroutine to use the iterator
go func() {
defer close(goroutineDone)
for msg := range iterSeq {
if msg == "hello" {
received = true
done <- true
return // Stop iteration — triggers Unsubscribe via yield returning false
}
}
}()
// Give time for the iterator to start
time.Sleep(100 * time.Millisecond)
// Publish a message
ps.Publish("test-ns", "hello")
// Wait for the message to be received or timeout
select {
case <-done:
if !received {
t.Fatal("Message was received but not 'hello'")
}
case <-time.After(time.Second):
t.Fatal("Timed out waiting for message")
}
// Wait for the goroutine to fully exit so Unsubscribe (triggered by the
// iterator cleanup when yield returns false) has completed.
select {
case <-goroutineDone:
case <-time.After(time.Second):
t.Fatal("Timed out waiting for goroutine to finish")
}
subCount := ps.SubscriberCount("test-ns")
if subCount != 0 {
t.Fatalf("Expected 0 receivers, got %d", subCount)
}
}
func TestPubSub_Publish(t *testing.T) {
ps := NewPubSub[string, string](10)
// Test publishing to a namespace with no subscribers
subs, receivers := ps.Publish("empty-ns", "hello")
if subs != 0 {
t.Fatalf("Expected 0 subscribers, got %d", subs)
}
if receivers != 0 {
t.Fatalf("Expected 0 receivers, got %d", receivers)
}
// Add a subscriber
ch, sub := ps.SubscribeByChan("test-ns", 1)
defer sub.Unsubscribe()
// Publish a message
subs, receivers = ps.Publish("test-ns", "hello")
if subs != 1 {
t.Fatalf("Expected 1 subscriber, got %d", subs)
}
if receivers != 1 {
t.Fatalf("Expected 1 receiver, got %d", receivers)
}
// Verify the message was received
select {
case msg := <-ch:
if msg != "hello" {
t.Fatalf("Expected to receive 'hello', got '%s'", msg)
}
case <-time.After(time.Second):
t.Fatal("Timed out waiting for message")
}
// Test non-blocking behavior with a full channel
// First fill the channel
ps.Publish("test-ns", "fill")
// Now publish again - this should not block but skip the receiver
subs, receivers = ps.Publish("test-ns", "overflow")
if subs != 1 {
t.Fatalf("Expected 1 subscriber, got %d", subs)
}
// The receiver count might be 0 if the channel is full
// Drain the channel
<-ch
}
func TestPubSub_PublishWithTimeout(t *testing.T) {
ps := NewPubSub[string, string](10)
// Add a subscriber with a channel
ch, sub := ps.SubscribeByChan("test-ns", 1)
defer sub.Unsubscribe()
// Publish with a timeout
subs, receivers := ps.PublishWithTimeout("test-ns", "hello", 100*time.Millisecond)
if subs != 1 {
t.Fatalf("Expected 1 subscriber, got %d", subs)
}
if receivers != 1 {
t.Fatalf("Expected 1 receiver, got %d", receivers)
}
// Verify the message was received
select {
case msg := <-ch:
if msg != "hello" {
t.Fatalf("Expected to receive 'hello', got '%s'", msg)
}
case <-time.After(time.Second):
t.Fatal("Timed out waiting for message")
}
// Fill the channel
ps.Publish("test-ns", "fill")
// Test timeout behavior with a full channel
start := time.Now()
subs, receivers = ps.PublishWithTimeout("test-ns", "timeout-test", 50*time.Millisecond)
elapsed := time.Since(start)
if subs != 1 {
t.Fatalf("Expected 1 subscriber, got %d", subs)
}
// The receiver count should be 0 if the timeout occurred
if elapsed < 50*time.Millisecond {
t.Fatalf("Expected to wait at least 50ms, only waited %v", elapsed)
}
// Drain the channel
<-ch
}
func TestPubSub_PublishWithContext(t *testing.T) {
ps := NewPubSub[string, string](10)
// Add a subscriber with a channel
ch, sub := ps.SubscribeByChan("test-ns", 1)
defer sub.Unsubscribe()
// Create a context
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
// Publish with context
subs, receivers, err := ps.PublishWithContext(ctx, "test-ns", "hello")
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if subs != 1 {
t.Fatalf("Expected 1 subscriber, got %d", subs)
}
if receivers != 1 {
t.Fatalf("Expected 1 receiver, got %d", receivers)
}
// Verify the message was received
select {
case msg := <-ch:
if msg != "hello" {
t.Fatalf("Expected to receive 'hello', got '%s'", msg)
}
case <-time.After(time.Second):
t.Fatal("Timed out waiting for message")
}
// Fill the channel
ps.Publish("test-ns", "fill")
// Test context cancellation with a full channel
ctx, cancel = context.WithCancel(context.Background())
// Cancel the context after a short delay
go func() {
time.Sleep(50 * time.Millisecond)
cancel()
}()
start := time.Now()
subs, receivers, err = ps.PublishWithContext(ctx, "test-ns", "context-test")
elapsed := time.Since(start)
if subs != 1 {
t.Fatalf("Expected 1 subscriber, got %d", subs)
}
// Should get a context canceled error
if err == nil {
t.Fatal("Expected context canceled error, got nil")
}
if elapsed < 50*time.Millisecond {
t.Fatalf("Expected to wait at least 50ms, only waited %v", elapsed)
}
// Drain the channel
<-ch
}
func TestPubSub_Unsubscribe(t *testing.T) {
ps := NewPubSub[string, string](10)
// Add a subscriber
ch, sub := ps.SubscribeByChan("test-ns", 1)
// Publish a message
subs, receivers := ps.Publish("test-ns", "hello")
if subs != 1 {
t.Fatalf("Expected 1 subscriber, got %d", subs)
}
if receivers != 1 {
t.Fatalf("Expected 1 receiver, got %d", receivers)
}
// Verify the message was received
select {
case msg := <-ch:
if msg != "hello" {
t.Fatalf("Expected to receive 'hello', got '%s'", msg)
}
case <-time.After(time.Second):
t.Fatal("Timed out waiting for message")
}
// Unsubscribe
sub.Unsubscribe()
// Publish again
subs, receivers = ps.Publish("test-ns", "after-unsub")
if subs != 0 {
t.Fatalf("Expected 0 subscribers after unsubscribe, got %d", subs)
}
if receivers != 0 {
t.Fatalf("Expected 0 receivers after unsubscribe, got %d", receivers)
}
// Check that the namespace is removed
namespaces := ps.Namespaces()
if len(namespaces) != 0 {
t.Fatalf("Expected 0 namespaces after unsubscribe, got %d", len(namespaces))
}
}
func TestPubSub_MultipleSubscribers(t *testing.T) {
ps := NewPubSub[string, string](10)
// Add multiple subscribers
ch1, sub1 := ps.SubscribeByChan("test-ns", 1)
defer sub1.Unsubscribe()
ch2, sub2 := ps.SubscribeByChan("test-ns", 1)
defer sub2.Unsubscribe()
var received string
var wg sync.WaitGroup
wg.Add(1)
sub3 := ps.SubscribeByCallback("test-ns", func(msg string) {
received = msg
wg.Done()
})
defer sub3.Unsubscribe()
// Publish a message
subs, receivers := ps.Publish("test-ns", "hello")
if subs != 3 {
t.Fatalf("Expected 3 subscribers, got %d", subs)
}
if receivers != 3 {
t.Fatalf("Expected 3 receivers, got %d", receivers)
}
// Verify the message was received by all subscribers
select {
case msg := <-ch1:
if msg != "hello" {
t.Fatalf("Expected ch1 to receive 'hello', got '%s'", msg)
}
case <-time.After(time.Second):
t.Fatal("Timed out waiting for message on ch1")
}
select {
case msg := <-ch2:
if msg != "hello" {
t.Fatalf("Expected ch2 to receive 'hello', got '%s'", msg)
}
case <-time.After(time.Second):
t.Fatal("Timed out waiting for message on ch2")
}
// Wait for the callback
wg.Wait()
if received != "hello" {
t.Fatalf("Expected callback to receive 'hello', got '%s'", received)
}
}
+2 -3
View File
@@ -2,7 +2,6 @@ package dataext
import ( import (
"errors" "errors"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"sync" "sync"
) )
@@ -64,7 +63,7 @@ func (s *Stack[T]) OptPop() *T {
result := s.data[l-1] result := s.data[l-1]
s.data = s.data[:l-1] s.data = s.data[:l-1]
return langext.Ptr(result) return new(result)
} }
func (s *Stack[T]) Peek() (T, error) { func (s *Stack[T]) Peek() (T, error) {
@@ -94,7 +93,7 @@ func (s *Stack[T]) OptPeek() *T {
return nil return nil
} }
return langext.Ptr(s.data[l-1]) return new(s.data[l-1])
} }
func (s *Stack[T]) Length() int { func (s *Stack[T]) Length() int {
+2 -2
View File
@@ -6,7 +6,7 @@ import (
"encoding/binary" "encoding/binary"
"errors" "errors"
"fmt" "fmt"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "git.blackforestbytes.com/BlackForestBytes/goext/langext"
"hash" "hash"
"io" "io"
"reflect" "reflect"
@@ -82,7 +82,7 @@ func binarize(writer io.Writer, dat reflect.Value, opt StructHashOptions) error
err = binary.Write(writer, binary.LittleEndian, uint8(dat.Kind())) err = binary.Write(writer, binary.LittleEndian, uint8(dat.Kind()))
switch dat.Kind() { switch dat.Kind() {
case reflect.Ptr, reflect.Map, reflect.Array, reflect.Chan, reflect.Slice, reflect.Interface: case reflect.Pointer, reflect.Map, reflect.Array, reflect.Chan, reflect.Slice, reflect.Interface:
if dat.IsNil() { if dat.IsNil() {
err = binary.Write(writer, binary.LittleEndian, uint64(0)) err = binary.Write(writer, binary.LittleEndian, uint64(0))
if err != nil { if err != nil {
+2 -3
View File
@@ -1,8 +1,7 @@
package dataext package dataext
import ( import (
"gogs.mikescher.com/BlackForestBytes/goext/langext" "git.blackforestbytes.com/BlackForestBytes/goext/tst"
"gogs.mikescher.com/BlackForestBytes/goext/tst"
"testing" "testing"
) )
@@ -46,7 +45,7 @@ func TestStructHashSimpleStruct(t *testing.T) {
tst.AssertHexEqual(t, "5d09090dc34ac59dd645f197a255f653387723de3afa1b614721ea5a081c675f", noErrStructHash(t, t0{ tst.AssertHexEqual(t, "5d09090dc34ac59dd645f197a255f653387723de3afa1b614721ea5a081c675f", noErrStructHash(t, t0{
F1: 10, F1: 10,
F2: []string{"1", "2", "3"}, F2: []string{"1", "2", "3"},
F3: langext.Ptr(99), F3: new(99),
})) }))
} }
+89
View File
@@ -2,6 +2,8 @@ package dataext
import "sync" import "sync"
// SyncMap is a thread-safe map implementation for generic key-value pairs.
// All functions aresafe to be called in parallel.
type SyncMap[TKey comparable, TData any] struct { type SyncMap[TKey comparable, TData any] struct {
data map[TKey]TData data map[TKey]TData
lock sync.Mutex lock sync.Mutex
@@ -11,6 +13,7 @@ func NewSyncMap[TKey comparable, TData any]() *SyncMap[TKey, TData] {
return &SyncMap[TKey, TData]{data: make(map[TKey]TData), lock: sync.Mutex{}} return &SyncMap[TKey, TData]{data: make(map[TKey]TData), lock: sync.Mutex{}}
} }
// Set sets the value for the provided key
func (s *SyncMap[TKey, TData]) Set(key TKey, data TData) { func (s *SyncMap[TKey, TData]) Set(key TKey, data TData) {
s.lock.Lock() s.lock.Lock()
defer s.lock.Unlock() defer s.lock.Unlock()
@@ -22,6 +25,7 @@ func (s *SyncMap[TKey, TData]) Set(key TKey, data TData) {
s.data[key] = data s.data[key] = data
} }
// SetIfNotContains sets the value for the provided key if it does not already exist
func (s *SyncMap[TKey, TData]) SetIfNotContains(key TKey, data TData) bool { func (s *SyncMap[TKey, TData]) SetIfNotContains(key TKey, data TData) bool {
s.lock.Lock() s.lock.Lock()
defer s.lock.Unlock() defer s.lock.Unlock()
@@ -39,6 +43,7 @@ func (s *SyncMap[TKey, TData]) SetIfNotContains(key TKey, data TData) bool {
return true return true
} }
// SetIfNotContainsFunc sets the value for the provided key using the provided function
func (s *SyncMap[TKey, TData]) SetIfNotContainsFunc(key TKey, data func() TData) bool { func (s *SyncMap[TKey, TData]) SetIfNotContainsFunc(key TKey, data func() TData) bool {
s.lock.Lock() s.lock.Lock()
defer s.lock.Unlock() defer s.lock.Unlock()
@@ -56,6 +61,7 @@ func (s *SyncMap[TKey, TData]) SetIfNotContainsFunc(key TKey, data func() TData)
return true return true
} }
// Get retrieves the value for the provided key
func (s *SyncMap[TKey, TData]) Get(key TKey) (TData, bool) { func (s *SyncMap[TKey, TData]) Get(key TKey) (TData, bool) {
s.lock.Lock() s.lock.Lock()
defer s.lock.Unlock() defer s.lock.Unlock()
@@ -71,6 +77,8 @@ func (s *SyncMap[TKey, TData]) Get(key TKey) (TData, bool) {
} }
} }
// GetAndSetIfNotContains returns the existing value if the key exists.
// Otherwise, it sets the provided value and returns it.
func (s *SyncMap[TKey, TData]) GetAndSetIfNotContains(key TKey, data TData) TData { func (s *SyncMap[TKey, TData]) GetAndSetIfNotContains(key TKey, data TData) TData {
s.lock.Lock() s.lock.Lock()
defer s.lock.Unlock() defer s.lock.Unlock()
@@ -87,6 +95,8 @@ func (s *SyncMap[TKey, TData]) GetAndSetIfNotContains(key TKey, data TData) TDat
} }
} }
// GetAndSetIfNotContainsFunc returns the existing value if the key exists.
// Otherwise, it calls the provided function to generate the value, sets it, and returns it.
func (s *SyncMap[TKey, TData]) GetAndSetIfNotContainsFunc(key TKey, data func() TData) TData { func (s *SyncMap[TKey, TData]) GetAndSetIfNotContainsFunc(key TKey, data func() TData) TData {
s.lock.Lock() s.lock.Lock()
defer s.lock.Unlock() defer s.lock.Unlock()
@@ -104,6 +114,7 @@ func (s *SyncMap[TKey, TData]) GetAndSetIfNotContainsFunc(key TKey, data func()
} }
} }
// Delete removes the entry with the provided key and returns true if the key existed before.
func (s *SyncMap[TKey, TData]) Delete(key TKey) bool { func (s *SyncMap[TKey, TData]) Delete(key TKey) bool {
s.lock.Lock() s.lock.Lock()
defer s.lock.Unlock() defer s.lock.Unlock()
@@ -119,6 +130,70 @@ func (s *SyncMap[TKey, TData]) Delete(key TKey) bool {
return ok return ok
} }
// DeleteIf deletes all entries that match the provided function and returns the number of removed entries.
func (s *SyncMap[TKey, TData]) DeleteIf(fn func(key TKey, data TData) bool) int {
s.lock.Lock()
defer s.lock.Unlock()
if s.data == nil {
s.data = make(map[TKey]TData)
}
rm := 0
for k, v := range s.data {
if fn(k, v) {
delete(s.data, k)
rm++
}
}
return rm
}
// UpdateIfExists updates the value if the key exists, otherwise it does nothing.
func (s *SyncMap[TKey, TData]) UpdateIfExists(key TKey, fn func(data TData) TData) bool {
s.lock.Lock()
defer s.lock.Unlock()
if s.data == nil {
s.data = make(map[TKey]TData)
}
if v, ok := s.data[key]; ok {
s.data[key] = fn(v)
return true
} else {
return false
}
}
// UpdateOrInsert updates the value if the key exists, otherwise it inserts the provided `insertValue`.
func (s *SyncMap[TKey, TData]) UpdateOrInsert(key TKey, fn func(data TData) TData, insertValue TData) bool {
s.lock.Lock()
defer s.lock.Unlock()
if s.data == nil {
s.data = make(map[TKey]TData)
}
if v, ok := s.data[key]; ok {
s.data[key] = fn(v)
return true
} else {
s.data[key] = insertValue
return false
}
}
// Clear removes all entries from the map.
func (s *SyncMap[TKey, TData]) Clear() {
s.lock.Lock()
defer s.lock.Unlock()
s.data = make(map[TKey]TData)
}
// Contains checks if the map contains the provided key.
func (s *SyncMap[TKey, TData]) Contains(key TKey) bool { func (s *SyncMap[TKey, TData]) Contains(key TKey) bool {
s.lock.Lock() s.lock.Lock()
defer s.lock.Unlock() defer s.lock.Unlock()
@@ -132,6 +207,7 @@ func (s *SyncMap[TKey, TData]) Contains(key TKey) bool {
return ok return ok
} }
// GetAllKeys returns a copy (!) of all keys in the map.
func (s *SyncMap[TKey, TData]) GetAllKeys() []TKey { func (s *SyncMap[TKey, TData]) GetAllKeys() []TKey {
s.lock.Lock() s.lock.Lock()
defer s.lock.Unlock() defer s.lock.Unlock()
@@ -149,6 +225,7 @@ func (s *SyncMap[TKey, TData]) GetAllKeys() []TKey {
return r return r
} }
// GetAllValues returns a copy (!) of all values in the map.
func (s *SyncMap[TKey, TData]) GetAllValues() []TData { func (s *SyncMap[TKey, TData]) GetAllValues() []TData {
s.lock.Lock() s.lock.Lock()
defer s.lock.Unlock() defer s.lock.Unlock()
@@ -165,3 +242,15 @@ func (s *SyncMap[TKey, TData]) GetAllValues() []TData {
return r return r
} }
// Count returns the number of entries in the map.
func (s *SyncMap[TKey, TData]) Count() int {
s.lock.Lock()
defer s.lock.Unlock()
if s.data == nil {
s.data = make(map[TKey]TData)
}
return len(s.data)
}
+24
View File
@@ -1,5 +1,9 @@
package enums package enums
import "maps"
import "encoding/json"
type Enum interface { type Enum interface {
Valid() bool Valid() bool
ValuesAny() []any ValuesAny() []any
@@ -31,3 +35,23 @@ type EnumDescriptionMetaValue struct {
Value Enum `json:"value"` Value Enum `json:"value"`
Description string `json:"description"` Description string `json:"description"`
} }
type EnumDataMetaValue struct {
VarName string `json:"varName"`
Value Enum `json:"value"`
Description *string `json:"description"`
Data map[string]any `json:"-"` //handled by MarshalJSON
}
func (v EnumDataMetaValue) MarshalJSON() ([]byte, error) {
m := make(map[string]any, 8)
maps.Copy(m, v.Data)
m["varName"] = v.VarName
m["value"] = v.Value
m["description"] = v.Description
return json.Marshal(m)
}
+300
View File
@@ -0,0 +1,300 @@
package excelext
import (
"reflect"
"git.blackforestbytes.com/BlackForestBytes/goext/dataext"
"git.blackforestbytes.com/BlackForestBytes/goext/exerr"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
excelize360 "github.com/360EntSecGroup-Skylar/excelize"
"github.com/xuri/excelize/v2"
)
type excelMapperColDefinition[T any] struct {
style *int
header string
width *float64
fn func(T) (any, error)
}
type ExcelMapper[T any] struct {
StyleDate *int
StyleDatetime *int
StyleEUR *int
StylePercentage *int
StyleHeader *int
StyleWSHeader *int
SkipColumnHeader bool
sheetName string
wsHeader []dataext.Tuple[string, *int]
colDefinitions []excelMapperColDefinition[T]
colFilter []func(v T) bool
}
func NewExcelMapper[T any]() (*ExcelMapper[T], error) {
em := &ExcelMapper[T]{
StyleDate: nil,
StyleDatetime: nil,
StyleEUR: nil,
StylePercentage: nil,
StyleHeader: nil,
StyleWSHeader: nil,
sheetName: "",
SkipColumnHeader: false,
wsHeader: make([]dataext.Tuple[string, *int], 0),
colDefinitions: make([]excelMapperColDefinition[T], 0),
}
return em, nil
}
func (em *ExcelMapper[T]) InitNewFile(sheetName string) (*excelize.File, error) {
f := excelize.NewFile()
defSheet := f.GetSheetList()[0]
sheet1 := sheetName
sheetIdx, err := f.NewSheet(sheet1)
if err != nil {
return nil, err
}
f.SetActiveSheet(sheetIdx)
err = f.DeleteSheet(defSheet)
err = em.InitStyles(f)
if err != nil {
return nil, err
}
return f, nil
}
func (em *ExcelMapper[T]) InitStyles(f *excelize.File) error {
styleDate, err := f.NewStyle(&excelize.Style{
CustomNumFmt: new("dd.mm.yyyy"),
})
if err != nil {
return err
}
styleDatetime, err := f.NewStyle(&excelize.Style{
NumFmt: 22,
})
if err != nil {
return err
}
styleEUR, err := f.NewStyle(&excelize.Style{
NumFmt: 218,
})
if err != nil {
return err
}
stylePercentage, err := f.NewStyle(&excelize.Style{
NumFmt: 10,
})
if err != nil {
return err
}
styleHeader, err := f.NewStyle(&excelize.Style{
Font: &excelize.Font{Bold: true, Size: 11},
})
if err != nil {
return err
}
styleWSHeader, err := f.NewStyle(&excelize.Style{
Font: &excelize.Font{Bold: true, Size: 24},
})
if err != nil {
return err
}
em.StyleDate = &styleDate
em.StyleDatetime = &styleDatetime
em.StyleEUR = &styleEUR
em.StylePercentage = &stylePercentage
em.StyleHeader = &styleHeader
em.StyleWSHeader = &styleWSHeader
return nil
}
func (em *ExcelMapper[T]) AddWorksheetHeader(header string, style *int) {
em.wsHeader = append(em.wsHeader, dataext.NewTuple(header, style))
}
func (em *ExcelMapper[T]) AddColumn(header string, style *int, width *float64, fn func(T) any) {
em.colDefinitions = append(em.colDefinitions, excelMapperColDefinition[T]{
style: style,
header: header,
width: width,
fn: func(t T) (any, error) { return fn(t), nil },
})
}
func (em *ExcelMapper[T]) AddColumnErr(header string, style *int, width *float64, fn func(T) (any, error)) {
em.colDefinitions = append(em.colDefinitions, excelMapperColDefinition[T]{
style: style,
header: header,
width: width,
fn: fn,
})
}
func (em *ExcelMapper[T]) Build(sheetName string, data []T) ([]byte, error) {
f, err := em.InitNewFile(sheetName)
if err != nil {
return nil, exerr.Wrap(err, "failed to init new file").Build()
}
err = em.BuildSingleSheet(f, sheetName, data)
if err != nil {
return nil, exerr.Wrap(err, "").Build()
}
buffer, err := f.WriteToBuffer()
if err != nil {
return nil, exerr.Wrap(err, "failed to build xls").Build()
}
return buffer.Bytes(), nil
}
func (em *ExcelMapper[T]) BuildSingleSheet(f *excelize.File, sheetName string, data []T) error {
if em.StyleHeader == nil || em.StyleDate == nil || em.StyleDatetime == nil || em.StyleEUR == nil || em.StylePercentage == nil || em.StyleWSHeader == nil {
err := em.InitStyles(f)
if err != nil {
return exerr.Wrap(err, "failed to init styles").Build()
}
}
rowOffset := 0
if len(em.wsHeader) > 0 {
for range em.wsHeader {
rowOffset += 1
}
rowOffset += 1
}
if !em.SkipColumnHeader {
for i, col := range em.colDefinitions {
err := f.SetCellValue(sheetName, c(rowOffset+1, i), col.header)
if err != nil {
return err
}
}
}
for i, col := range em.colDefinitions {
if col.style != nil {
err := f.SetColStyle(sheetName, excelize360.ToAlphaString(i), *col.style)
if err != nil {
return err
}
}
}
for i, col := range em.colDefinitions {
if col.width != nil {
err := f.SetColWidth(sheetName, excelize360.ToAlphaString(i), excelize360.ToAlphaString(i), *col.width)
if err != nil {
return err
}
}
}
err := f.SetRowStyle(sheetName, rowOffset+1, rowOffset+1, *em.StyleHeader)
if err != nil {
return err
}
if len(em.wsHeader) > 0 {
for i, hdr := range em.wsHeader {
style := *langext.CoalesceOpt(hdr.V2, em.StyleWSHeader)
err = f.SetCellValue(sheetName, c(i+1, 0), hdr.V1)
if err != nil {
return err
}
err = f.MergeCell(sheetName, c(i+1, 0), c(i+1, len(em.colDefinitions)-1))
if err != nil {
return err
}
err = f.SetRowStyle(sheetName, 1, 1, style)
if err != nil {
return err
}
}
}
iRow := rowOffset + 1
if !em.SkipColumnHeader {
iRow += 1
}
for _, dat := range data {
skip := false
for _, filter := range em.colFilter {
if !filter(dat) {
skip = true
break
}
}
if skip {
continue
}
for iCol, col := range em.colDefinitions {
cellVal, err := col.fn(dat)
if err != nil {
return err
}
for reflect.ValueOf(cellVal).Kind() == reflect.Pointer && !reflect.ValueOf(cellVal).IsNil() {
cellVal = reflect.ValueOf(cellVal).Elem().Interface()
}
if langext.IsNil(cellVal) {
err = f.SetCellValue(sheetName, c(iRow, iCol), "")
if err != nil {
return err
}
} else {
err = f.SetCellValue(sheetName, c(iRow, iCol), cellVal)
if err != nil {
return err
}
}
}
iRow++
}
//for i, col := range em.colDefinitions {
// if col.width == nil {
// //TODO https://github.com/qax-os/excelize/pull/1386
// }
//}
return nil
}
func (em *ExcelMapper[T]) AddFilter(f func(v T) bool) {
em.colFilter = append(em.colFilter, f)
}
+26
View File
@@ -0,0 +1,26 @@
package excelext
import (
"strconv"
"git.blackforestbytes.com/BlackForestBytes/goext/rfctime"
"github.com/360EntSecGroup-Skylar/excelize"
)
func c(row int, col int) string {
return excelize.ToAlphaString(col) + strconv.Itoa(row)
}
func excelizeOptTime(t *rfctime.RFC3339NanoTime) any {
if t == nil {
return ""
}
return t.Time()
}
func excelizeOptDate(t *rfctime.Date) any {
if t == nil {
return ""
}
return t.TimeUTC()
}
+72 -16
View File
@@ -5,17 +5,18 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog"
"go.mongodb.org/mongo-driver/bson/primitive"
"gogs.mikescher.com/BlackForestBytes/goext/dataext"
"gogs.mikescher.com/BlackForestBytes/goext/enums"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"net/http" "net/http"
"os" "os"
"runtime/debug" "runtime/debug"
"strings" "strings"
"time" "time"
"git.blackforestbytes.com/BlackForestBytes/goext/dataext"
"git.blackforestbytes.com/BlackForestBytes/goext/enums"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog"
"go.mongodb.org/mongo-driver/bson/primitive"
) )
// //
@@ -276,11 +277,11 @@ func (b *Builder) Ints32(key string, val []int32) *Builder {
return b.addMeta(key, MDTInt32Array, val) return b.addMeta(key, MDTInt32Array, val)
} }
func (b *Builder) Type(key string, cls interface{}) *Builder { func (b *Builder) Type(key string, cls any) *Builder {
return b.addMeta(key, MDTString, fmt.Sprintf("%T", cls)) return b.addMeta(key, MDTString, fmt.Sprintf("%T", cls))
} }
func (b *Builder) Interface(key string, val interface{}) *Builder { func (b *Builder) Interface(key string, val any) *Builder {
return b.addMeta(key, MDTAny, newAnyWrap(val)) return b.addMeta(key, MDTAny, newAnyWrap(val))
} }
@@ -430,12 +431,22 @@ func (b *Builder) BuildAsExerr(ctxs ...context.Context) *ExErr {
return FromError(b.wrappedErr) return FromError(b.wrappedErr)
} }
if pkgconfig.ZeroLogErrTraces && !b.noLog && (b.errorData.Severity == SevErr || b.errorData.Severity == SevFatal) { if pkgconfig.ZeroLogErrTraces && !b.noLog {
b.errorData.ShortLog(pkgconfig.ZeroLogger.Error()) if b.errorData.Severity == SevErr || b.errorData.Severity == SevFatal {
} else if pkgconfig.ZeroLogAllTraces && !b.noLog { b.errorData.ShortLog(pkgconfig.ZeroLogger.Error())
b.errorData.ShortLog(pkgconfig.ZeroLogger.Error()) } else if b.errorData.Severity == SevWarn {
} b.errorData.ShortLog(pkgconfig.ZeroLogger.Warn())
} else if b.errorData.Severity == SevInfo {
b.errorData.ShortLog(pkgconfig.ZeroLogger.Info())
} else if b.errorData.Severity == SevDebug {
b.errorData.ShortLog(pkgconfig.ZeroLogger.Debug())
} else if b.errorData.Severity == SevTrace {
b.errorData.ShortLog(pkgconfig.ZeroLogger.Trace())
} else {
b.errorData.ShortLog(pkgconfig.ZeroLogger.Error()) // ?!? unknown severity
}
}
b.errorData.CallListener(MethodBuild, ListenerOpt{NoLog: b.noLog}) b.errorData.CallListener(MethodBuild, ListenerOpt{NoLog: b.noLog})
return b.errorData return b.errorData
@@ -445,6 +456,8 @@ func (b *Builder) BuildAsExerr(ctxs ...context.Context) *ExErr {
// The error also gets printed to stdout/stderr // The error also gets printed to stdout/stderr
// If the error is SevErr|SevFatal we also send it to the error-service // If the error is SevErr|SevFatal we also send it to the error-service
func (b *Builder) Output(ctx context.Context, g *gin.Context) { func (b *Builder) Output(ctx context.Context, g *gin.Context) {
warnOnPkgConfigNotInitialized()
if !b.containsGinData && g.Request != nil { if !b.containsGinData && g.Request != nil {
// Auto-Add gin metadata if the caller hasn't already done it // Auto-Add gin metadata if the caller hasn't already done it
b.GinReq(ctx, g, g.Request) b.GinReq(ctx, g, g.Request)
@@ -452,6 +465,21 @@ func (b *Builder) Output(ctx context.Context, g *gin.Context) {
b.CtxData(MethodOutput, ctx) b.CtxData(MethodOutput, ctx)
// this is only here to add one level to the trace
// so that .Build() and .Output() and .Print() have the same depth and our stack-skip logger can have the same skip-count
b.doGinOutput(ctx, g)
}
// OutputRaw works teh same as Output() - but does not depend on gin and works with a raw http.ResponseWriter
func (b *Builder) OutputRaw(w http.ResponseWriter) {
warnOnPkgConfigNotInitialized()
// this is only here to add one level to the trace
// so that .Build() and .Output() and .Print() have the same depth and our stack-skip logger can have the same skip-count
b.doRawOutput(w)
}
func (b *Builder) doGinOutput(ctx context.Context, g *gin.Context) {
b.errorData.Output(g) b.errorData.Output(g)
if (b.errorData.Severity == SevErr || b.errorData.Severity == SevFatal) && (pkgconfig.ZeroLogErrGinOutput || pkgconfig.ZeroLogAllGinOutput) { if (b.errorData.Severity == SevErr || b.errorData.Severity == SevFatal) && (pkgconfig.ZeroLogErrGinOutput || pkgconfig.ZeroLogAllGinOutput) {
@@ -463,6 +491,18 @@ func (b *Builder) Output(ctx context.Context, g *gin.Context) {
b.errorData.CallListener(MethodOutput, ListenerOpt{NoLog: b.noLog}) b.errorData.CallListener(MethodOutput, ListenerOpt{NoLog: b.noLog})
} }
func (b *Builder) doRawOutput(w http.ResponseWriter) {
b.errorData.OutputRaw(w)
if (b.errorData.Severity == SevErr || b.errorData.Severity == SevFatal) && (pkgconfig.ZeroLogErrGinOutput || pkgconfig.ZeroLogAllGinOutput) {
b.errorData.Log(pkgconfig.ZeroLogger.Error())
} else if (b.errorData.Severity == SevWarn) && (pkgconfig.ZeroLogAllGinOutput) {
b.errorData.Log(pkgconfig.ZeroLogger.Warn())
}
b.errorData.CallListener(MethodOutput, ListenerOpt{NoLog: b.noLog})
}
// Print prints the error // Print prints the error
// If the error is SevErr we also send it to the error-service // If the error is SevErr we also send it to the error-service
func (b *Builder) Print(ctxs ...context.Context) Proxy { func (b *Builder) Print(ctxs ...context.Context) Proxy {
@@ -472,14 +512,24 @@ func (b *Builder) Print(ctxs ...context.Context) Proxy {
b.CtxData(MethodPrint, dctx) b.CtxData(MethodPrint, dctx)
} }
// this is only here to add one level to the trace
// so that .Build() and .Output() and .Print() have the same depth and our stack-skip logger can have the same skip-count
return b.doPrint()
}
func (b *Builder) doPrint() Proxy {
if b.errorData.Severity == SevErr || b.errorData.Severity == SevFatal { if b.errorData.Severity == SevErr || b.errorData.Severity == SevFatal {
b.errorData.Log(pkgconfig.ZeroLogger.Error()) b.errorData.Log(pkgconfig.ZeroLogger.Error())
} else if b.errorData.Severity == SevWarn { } else if b.errorData.Severity == SevWarn {
b.errorData.ShortLog(pkgconfig.ZeroLogger.Warn()) b.errorData.ShortLog(pkgconfig.ZeroLogger.Warn())
} else if b.errorData.Severity == SevInfo { } else if b.errorData.Severity == SevInfo {
b.errorData.ShortLog(pkgconfig.ZeroLogger.Info()) b.errorData.ShortLog(pkgconfig.ZeroLogger.Info())
} else { } else if b.errorData.Severity == SevDebug {
b.errorData.ShortLog(pkgconfig.ZeroLogger.Debug()) b.errorData.ShortLog(pkgconfig.ZeroLogger.Debug())
} else if b.errorData.Severity == SevTrace {
b.errorData.ShortLog(pkgconfig.ZeroLogger.Trace())
} else {
b.errorData.Log(pkgconfig.ZeroLogger.Error()) // ?!? unknown severity
} }
b.errorData.CallListener(MethodPrint, ListenerOpt{NoLog: b.noLog}) b.errorData.CallListener(MethodPrint, ListenerOpt{NoLog: b.noLog})
@@ -501,16 +551,22 @@ func (b *Builder) Fatal(ctxs ...context.Context) {
b.CtxData(MethodFatal, dctx) b.CtxData(MethodFatal, dctx)
} }
b.errorData.Log(pkgconfig.ZeroLogger.WithLevel(zerolog.FatalLevel)) // this is only here to add one level to the trace
// so that .Build() and .Output() and .Print() have the same depth and our stack-skip logger can have the same skip-count
b.doLogFatal()
b.errorData.CallListener(MethodFatal, ListenerOpt{NoLog: b.noLog}) b.errorData.CallListener(MethodFatal, ListenerOpt{NoLog: b.noLog})
os.Exit(1) os.Exit(1)
} }
func (b *Builder) doLogFatal() {
b.errorData.Log(pkgconfig.ZeroLogger.WithLevel(zerolog.FatalLevel))
}
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
func (b *Builder) addMeta(key string, mdtype metaDataType, val interface{}) *Builder { func (b *Builder) addMeta(key string, mdtype metaDataType, val any) *Builder {
b.errorData.Meta.add(key, mdtype, val) b.errorData.Meta.add(key, mdtype, val)
return b return b
} }
+8 -11
View File
@@ -3,13 +3,14 @@ package exerr
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
"go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/bson/primitive"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "maps"
"reflect" "reflect"
"time" "time"
) )
var reflectTypeStr = reflect.TypeOf("") var reflectTypeStr = reflect.TypeFor[string]()
func FromError(err error) *ExErr { func FromError(err error) *ExErr {
@@ -152,20 +153,18 @@ func getForeignMeta(err error) (mm MetaMap) {
}() }()
rval := reflect.ValueOf(err) rval := reflect.ValueOf(err)
if rval.Kind() == reflect.Interface || rval.Kind() == reflect.Ptr { if rval.Kind() == reflect.Interface || rval.Kind() == reflect.Pointer {
rval = reflect.ValueOf(err).Elem() rval = reflect.ValueOf(err).Elem()
} }
mm.add("foreign.errortype", MDTString, rval.Type().String()) mm.add("foreign.errortype", MDTString, rval.Type().String())
for k, v := range addMetaPrefix("foreign", getReflectedMetaValues(err, 8)) { maps.Copy(mm, addMetaPrefix("foreign", getReflectedMetaValues(err, 8)))
mm[k] = v
}
return mm return mm
} }
func getReflectedMetaValues(value interface{}, remainingDepth int) map[string]MetaValue { func getReflectedMetaValues(value any, remainingDepth int) map[string]MetaValue {
if remainingDepth <= 0 { if remainingDepth <= 0 {
return map[string]MetaValue{} return map[string]MetaValue{}
@@ -177,7 +176,7 @@ func getReflectedMetaValues(value interface{}, remainingDepth int) map[string]Me
rval := reflect.ValueOf(value) rval := reflect.ValueOf(value)
if rval.Type().Kind() == reflect.Ptr { if rval.Type().Kind() == reflect.Pointer {
if rval.IsNil() { if rval.IsNil() {
return map[string]MetaValue{"*": {DataType: MDTNil, Value: nil}} return map[string]MetaValue{"*": {DataType: MDTNil, Value: nil}}
@@ -237,9 +236,7 @@ func getReflectedMetaValues(value interface{}, remainingDepth int) map[string]Me
fieldname := fieldtype.Name fieldname := fieldtype.Name
if fieldtype.IsExported() { if fieldtype.IsExported() {
for k, v := range addMetaPrefix(fieldname, getReflectedMetaValues(rval.Field(i).Interface(), remainingDepth-1)) { maps.Copy(m, addMetaPrefix(fieldname, getReflectedMetaValues(rval.Field(i).Interface(), remainingDepth-1)))
m[k] = v
}
} }
} }
return m return m
+3 -3
View File
@@ -54,7 +54,7 @@ func (e ErrorCategory) MarshalBSONValue() (bsontype.Type, []byte, error) {
} }
func (e ErrorCategory) DecodeValue(dc bsoncodec.DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error { func (e ErrorCategory) DecodeValue(dc bsoncodec.DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error {
if val.Kind() == reflect.Ptr && val.IsNil() { if val.Kind() == reflect.Pointer && val.IsNil() {
if !val.CanSet() { if !val.CanSet() {
return errors.New("ValueUnmarshalerDecodeValue") return errors.New("ValueUnmarshalerDecodeValue")
} }
@@ -66,7 +66,7 @@ func (e ErrorCategory) DecodeValue(dc bsoncodec.DecodeContext, vr bsonrw.ValueRe
return err return err
} }
if val.Kind() == reflect.Ptr && len(src) == 0 { if val.Kind() == reflect.Pointer && len(src) == 0 {
val.Set(reflect.Zero(val.Type())) val.Set(reflect.Zero(val.Type()))
return nil return nil
} }
@@ -76,7 +76,7 @@ func (e ErrorCategory) DecodeValue(dc bsoncodec.DecodeContext, vr bsonrw.ValueRe
return err return err
} }
if val.Kind() == reflect.Ptr { if val.Kind() == reflect.Pointer {
val.Set(reflect.ValueOf(&e)) val.Set(reflect.ValueOf(&e))
} else { } else {
val.Set(reflect.ValueOf(e)) val.Set(reflect.ValueOf(e))
+3 -3
View File
@@ -56,7 +56,7 @@ func (e ErrorSeverity) MarshalBSONValue() (bsontype.Type, []byte, error) {
} }
func (e ErrorSeverity) DecodeValue(dc bsoncodec.DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error { func (e ErrorSeverity) DecodeValue(dc bsoncodec.DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error {
if val.Kind() == reflect.Ptr && val.IsNil() { if val.Kind() == reflect.Pointer && val.IsNil() {
if !val.CanSet() { if !val.CanSet() {
return errors.New("ValueUnmarshalerDecodeValue") return errors.New("ValueUnmarshalerDecodeValue")
} }
@@ -68,7 +68,7 @@ func (e ErrorSeverity) DecodeValue(dc bsoncodec.DecodeContext, vr bsonrw.ValueRe
return err return err
} }
if val.Kind() == reflect.Ptr && len(src) == 0 { if val.Kind() == reflect.Pointer && len(src) == 0 {
val.Set(reflect.Zero(val.Type())) val.Set(reflect.Zero(val.Type()))
return nil return nil
} }
@@ -78,7 +78,7 @@ func (e ErrorSeverity) DecodeValue(dc bsoncodec.DecodeContext, vr bsonrw.ValueRe
return err return err
} }
if val.Kind() == reflect.Ptr { if val.Kind() == reflect.Pointer {
val.Set(reflect.ValueOf(&e)) val.Set(reflect.ValueOf(&e))
} else { } else {
val.Set(reflect.ValueOf(e)) val.Set(reflect.ValueOf(e))
+33 -31
View File
@@ -4,13 +4,13 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"reflect"
"git.blackforestbytes.com/BlackForestBytes/goext/dataext"
"go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/bsoncodec" "go.mongodb.org/mongo-driver/bson/bsoncodec"
"go.mongodb.org/mongo-driver/bson/bsonrw" "go.mongodb.org/mongo-driver/bson/bsonrw"
"go.mongodb.org/mongo-driver/bson/bsontype" "go.mongodb.org/mongo-driver/bson/bsontype"
"gogs.mikescher.com/BlackForestBytes/goext/dataext"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"reflect"
) )
type ErrorType struct { type ErrorType struct {
@@ -20,40 +20,42 @@ type ErrorType struct {
//goland:noinspection GoUnusedGlobalVariable //goland:noinspection GoUnusedGlobalVariable
var ( var (
TypeInternal = NewType("INTERNAL_ERROR", langext.Ptr(500)) TypeInternal = NewType("INTERNAL_ERROR", new(500))
TypePanic = NewType("PANIC", langext.Ptr(500)) TypePanic = NewType("PANIC", new(500))
TypeNotImplemented = NewType("NOT_IMPLEMENTED", langext.Ptr(500)) TypeNotImplemented = NewType("NOT_IMPLEMENTED", new(500))
TypeAssert = NewType("ASSERT", langext.Ptr(500)) TypeAssert = NewType("ASSERT", new(500))
TypeMongoQuery = NewType("MONGO_QUERY", langext.Ptr(500)) TypeMongoQuery = NewType("MONGO_QUERY", new(500))
TypeCursorTokenDecode = NewType("CURSOR_TOKEN_DECODE", langext.Ptr(500)) TypeCursorTokenDecode = NewType("CURSOR_TOKEN_DECODE", new(500))
TypeMongoFilter = NewType("MONGO_FILTER", langext.Ptr(500)) TypeMongoFilter = NewType("MONGO_FILTER", new(500))
TypeMongoReflection = NewType("MONGO_REFLECTION", langext.Ptr(500)) TypeMongoReflection = NewType("MONGO_REFLECTION", new(500))
TypeMongoInvalidOpt = NewType("MONGO_INVALIDOPT", langext.Ptr(500)) TypeMongoInvalidOpt = NewType("MONGO_INVALIDOPT", new(500))
TypeSQLQuery = NewType("SQL_QUERY", langext.Ptr(500)) TypeSQLQuery = NewType("SQL_QUERY", new(500))
TypeSQLBuild = NewType("SQL_BUILD", langext.Ptr(500)) TypeSQLBuild = NewType("SQL_BUILD", new(500))
TypeSQLDecode = NewType("SQL_DECODE", langext.Ptr(500)) TypeSQLDecode = NewType("SQL_DECODE", new(500))
TypeWrap = NewType("Wrap", nil) TypeWrap = NewType("Wrap", nil)
TypeBindFailURI = NewType("BINDFAIL_URI", langext.Ptr(400)) TypeBindFailURI = NewType("BINDFAIL_URI", new(400))
TypeBindFailQuery = NewType("BINDFAIL_QUERY", langext.Ptr(400)) TypeBindFailQuery = NewType("BINDFAIL_QUERY", new(400))
TypeBindFailJSON = NewType("BINDFAIL_JSON", langext.Ptr(400)) TypeBindFailJSON = NewType("BINDFAIL_JSON", new(400))
TypeBindFailFormData = NewType("BINDFAIL_FORMDATA", langext.Ptr(400)) TypeBindFailFormData = NewType("BINDFAIL_FORMDATA", new(400))
TypeBindFailHeader = NewType("BINDFAIL_HEADER", langext.Ptr(400)) TypeBindFailHeader = NewType("BINDFAIL_HEADER", new(400))
TypeMarshalEntityID = NewType("MARSHAL_ENTITY_ID", langext.Ptr(400)) TypeMarshalEntityID = NewType("MARSHAL_ENTITY_ID", new(400))
TypeInvalidCSID = NewType("INVALID_CSID", langext.Ptr(400)) TypeInvalidCSID = NewType("INVALID_CSID", new(400))
TypeGoogleStatuscode = NewType("GOOGLE_STATUSCODE", langext.Ptr(400)) TypeGoogleStatuscode = NewType("GOOGLE_STATUSCODE", new(400))
TypeGoogleResponse = NewType("GOOGLE_RESPONSE", langext.Ptr(400)) TypeGoogleResponse = NewType("GOOGLE_RESPONSE", new(400))
TypeUnauthorized = NewType("UNAUTHORIZED", langext.Ptr(401)) TypeUnauthorized = NewType("UNAUTHORIZED", new(401))
TypeAuthFailed = NewType("AUTH_FAILED", langext.Ptr(401)) TypeAuthFailed = NewType("AUTH_FAILED", new(401))
TypeInvalidImage = NewType("IMAGEEXT_INVALID_IMAGE", langext.Ptr(400)) TypeInvalidImage = NewType("IMAGEEXT_INVALID_IMAGE", new(400))
TypeInvalidMimeType = NewType("IMAGEEXT_INVALID_MIMETYPE", langext.Ptr(400)) TypeInvalidMimeType = NewType("IMAGEEXT_INVALID_MIMETYPE", new(400))
TypeWebsocket = NewType("WEBSOCKET", new(500))
// other values come from the downstream application that uses goext // other values come from the downstream application that uses goext
) )
@@ -110,7 +112,7 @@ func (e ErrorType) MarshalBSONValue() (bsontype.Type, []byte, error) {
} }
func (e ErrorType) DecodeValue(dc bsoncodec.DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error { func (e ErrorType) DecodeValue(dc bsoncodec.DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error {
if val.Kind() == reflect.Ptr && val.IsNil() { if val.Kind() == reflect.Pointer && val.IsNil() {
if !val.CanSet() { if !val.CanSet() {
return errors.New("ValueUnmarshalerDecodeValue") return errors.New("ValueUnmarshalerDecodeValue")
} }
@@ -122,7 +124,7 @@ func (e ErrorType) DecodeValue(dc bsoncodec.DecodeContext, vr bsonrw.ValueReader
return err return err
} }
if val.Kind() == reflect.Ptr && len(src) == 0 { if val.Kind() == reflect.Pointer && len(src) == 0 {
val.Set(reflect.Zero(val.Type())) val.Set(reflect.Zero(val.Type()))
return nil return nil
} }
@@ -132,7 +134,7 @@ func (e ErrorType) DecodeValue(dc bsoncodec.DecodeContext, vr bsonrw.ValueReader
return err return err
} }
if val.Kind() == reflect.Ptr { if val.Kind() == reflect.Pointer {
val.Set(reflect.ValueOf(&e)) val.Set(reflect.ValueOf(&e))
} else { } else {
val.Set(reflect.ValueOf(e)) val.Set(reflect.ValueOf(e))
+1 -1
View File
@@ -6,7 +6,7 @@ import (
"go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo"
"gogs.mikescher.com/BlackForestBytes/goext/tst" "git.blackforestbytes.com/BlackForestBytes/goext/tst"
"testing" "testing"
"time" "time"
) )
+2 -2
View File
@@ -3,9 +3,9 @@ package exerr
import ( import (
"context" "context"
"fmt" "fmt"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"net/http" "net/http"
"os" "os"
) )
@@ -119,7 +119,7 @@ func newDefaultLogger() zerolog.Logger {
multi := zerolog.MultiLevelWriter(cw) multi := zerolog.MultiLevelWriter(cw)
return zerolog.New(multi).With().Timestamp().CallerWithSkipFrameCount(4).Logger() return zerolog.New(multi).With().Timestamp().CallerWithSkipFrameCount(5).Logger()
} }
func Initialized() bool { func Initialized() bool {
+35 -32
View File
@@ -2,12 +2,13 @@ package exerr
import ( import (
"fmt" "fmt"
"github.com/rs/xid"
"github.com/rs/zerolog"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"reflect" "reflect"
"strings" "strings"
"time" "time"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
"github.com/rs/xid"
"github.com/rs/zerolog"
) )
type ExErr struct { type ExErr struct {
@@ -125,35 +126,37 @@ func (ee *ExErr) FormatLog(lvl LogPrintLevel) string {
} else if lvl == LogPrintOverview { } else if lvl == LogPrintOverview {
str := "[" + ee.RecursiveType().Key + "] <" + ee.UniqueID + "> " + strings.ReplaceAll(ee.RecursiveMessage(), "\n", " ") + "\n" var str strings.Builder
str.WriteString("[" + ee.RecursiveType().Key + "] <" + ee.UniqueID + "> " + strings.ReplaceAll(ee.RecursiveMessage(), "\n", " ") + "\n")
for exk, exv := range ee.Extra { for exk, exv := range ee.Extra {
str += fmt.Sprintf(" # [[[ %s ==> %v ]]]\n", exk, exv) str.WriteString(fmt.Sprintf(" # [[[ %s ==> %v ]]]\n", exk, exv))
} }
indent := "" var indent strings.Builder
for curr := ee; curr != nil; curr = curr.OriginalError { for curr := ee; curr != nil; curr = curr.OriginalError {
indent += " " indent.WriteString(" ")
str += indent str.WriteString(indent.String())
str += "-> " str.WriteString("-> ")
strmsg := strings.Trim(curr.Message, " \r\n\t") strmsg := strings.Trim(curr.Message, " \r\n\t")
if lbidx := strings.Index(curr.Message, "\n"); lbidx >= 0 { if lbidx := strings.Index(curr.Message, "\n"); lbidx >= 0 {
strmsg = strmsg[0:lbidx] strmsg = strmsg[0:lbidx]
} }
strmsg = langext.StrLimit(strmsg, 61, "...") strmsg = langext.StrLimit(strmsg, 61, "...")
str += strmsg str.WriteString(strmsg)
str += "\n" str.WriteString("\n")
} }
return str return str.String()
} else if lvl == LogPrintFull { } else if lvl == LogPrintFull {
str := "[" + ee.RecursiveType().Key + "] <" + ee.UniqueID + "> " + strings.ReplaceAll(ee.RecursiveMessage(), "\n", " ") + "\n" var str strings.Builder
str.WriteString("[" + ee.RecursiveType().Key + "] <" + ee.UniqueID + "> " + strings.ReplaceAll(ee.RecursiveMessage(), "\n", " ") + "\n")
for exk, exv := range ee.Extra { for exk, exv := range ee.Extra {
str += fmt.Sprintf(" # [[[ %s ==> %v ]]]\n", exk, exv) str.WriteString(fmt.Sprintf(" # [[[ %s ==> %v ]]]\n", exk, exv))
} }
indent := "" indent := ""
@@ -165,33 +168,33 @@ func (ee *ExErr) FormatLog(lvl LogPrintLevel) string {
etype = "~" etype = "~"
} }
str += indent str.WriteString(indent)
str += "-> [" str.WriteString("-> [")
str += etype str.WriteString(etype)
if curr.Category == CatForeign { if curr.Category == CatForeign {
str += "|Foreign" str.WriteString("|Foreign")
} }
str += "] " str.WriteString("] ")
str += strings.ReplaceAll(curr.Message, "\n", " ") str.WriteString(strings.ReplaceAll(curr.Message, "\n", " "))
if curr.Caller != "" { if curr.Caller != "" {
str += " (@ " str.WriteString(" (@ ")
str += curr.Caller str.WriteString(curr.Caller)
str += ")" str.WriteString(")")
} }
str += "\n" str.WriteString("\n")
if curr.Meta.Any() { if curr.Meta.Any() {
meta := indent + " {" + curr.Meta.FormatOneLine(240) + "}" meta := indent + " {" + curr.Meta.FormatOneLine(240) + "}"
if len(meta) < 200 { if len(meta) < 200 {
str += meta str.WriteString(meta)
str += "\n" str.WriteString("\n")
} else { } else {
str += curr.Meta.FormatMultiLine(indent+" ", " ", 1024) str.WriteString(curr.Meta.FormatMultiLine(indent+" ", " ", 1024))
str += "\n" str.WriteString("\n")
} }
} }
} }
return str return str.String()
} else { } else {
@@ -201,7 +204,7 @@ func (ee *ExErr) FormatLog(lvl LogPrintLevel) string {
} }
func (ee *ExErr) ShortLog(evt *zerolog.Event) { func (ee *ExErr) ShortLog(evt *zerolog.Event) {
ee.Meta.Apply(evt, langext.Ptr(240)).Msg(ee.FormatLog(LogPrintShort)) ee.Meta.Apply(evt, new(240)).Msg(ee.FormatLog(LogPrintShort))
} }
// RecursiveMessage returns the message to show // RecursiveMessage returns the message to show
@@ -254,7 +257,7 @@ func (ee *ExErr) RecursiveType() ErrorType {
func (ee *ExErr) RecursiveStatuscode() *int { func (ee *ExErr) RecursiveStatuscode() *int {
for curr := ee; curr != nil; curr = curr.OriginalError { for curr := ee; curr != nil; curr = curr.OriginalError {
if curr.StatusCode != nil { if curr.StatusCode != nil {
return langext.Ptr(*curr.StatusCode) return new(*curr.StatusCode)
} }
} }
@@ -279,7 +282,7 @@ func (ee *ExErr) RecursiveCategory() ErrorCategory {
func (ee *ExErr) RecursiveMeta(key string) *MetaValue { func (ee *ExErr) RecursiveMeta(key string) *MetaValue {
for curr := ee; curr != nil; curr = curr.OriginalError { for curr := ee; curr != nil; curr = curr.OriginalError {
if metaval, ok := curr.Meta[key]; ok { if metaval, ok := curr.Meta[key]; ok {
return langext.Ptr(metaval) return new(metaval)
} }
} }
+2 -2
View File
@@ -2,8 +2,8 @@ package exerr
import ( import (
"errors" "errors"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "git.blackforestbytes.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/tst" "git.blackforestbytes.com/BlackForestBytes/goext/tst"
"os" "os"
"testing" "testing"
) )
+36 -6
View File
@@ -1,9 +1,11 @@
package exerr package exerr
import ( import (
"maps"
json "git.blackforestbytes.com/BlackForestBytes/goext/gojson"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
json "gogs.mikescher.com/BlackForestBytes/goext/gojson"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"net/http" "net/http"
"time" "time"
) )
@@ -50,9 +52,7 @@ func (ee *ExErr) toJson(depth int, applyExtendListener bool, outputMeta bool) la
ginJson["meta"] = metaJson ginJson["meta"] = metaJson
extraJson := langext.H{} extraJson := langext.H{}
for extraKey, extraVal := range ee.Extra { maps.Copy(extraJson, ee.Extra)
extraJson[extraKey] = extraVal
}
ginJson["extra"] = extraJson ginJson["extra"] = extraJson
} }
@@ -68,7 +68,6 @@ func (ee *ExErr) ToDefaultAPIJson() (string, error) {
gjr := json.GoJsonRender{Data: ee.ToAPIJson(true, pkgconfig.ExtendedGinOutput, pkgconfig.IncludeMetaInGinOutput), NilSafeSlices: true, NilSafeMaps: true} gjr := json.GoJsonRender{Data: ee.ToAPIJson(true, pkgconfig.ExtendedGinOutput, pkgconfig.IncludeMetaInGinOutput), NilSafeSlices: true, NilSafeMaps: true}
r, err := gjr.RenderString() r, err := gjr.RenderString()
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -143,3 +142,34 @@ func (ee *ExErr) Output(g *gin.Context) {
g.Render(statuscode, json.GoJsonRender{Data: ginOutput, NilSafeSlices: true, NilSafeMaps: true}) g.Render(statuscode, json.GoJsonRender{Data: ginOutput, NilSafeSlices: true, NilSafeMaps: true})
} }
func (ee *ExErr) OutputRaw(w http.ResponseWriter) {
warnOnPkgConfigNotInitialized()
var statuscode = http.StatusInternalServerError
var baseCat = ee.RecursiveCategory()
var baseType = ee.RecursiveType()
var baseStatuscode = ee.RecursiveStatuscode()
if baseCat == CatUser {
statuscode = http.StatusBadRequest
} else if baseCat == CatSystem {
statuscode = http.StatusInternalServerError
}
if baseStatuscode != nil {
statuscode = *ee.StatusCode
} else if baseType.DefaultStatusCode != nil {
statuscode = *baseType.DefaultStatusCode
}
ginOutput, err := ee.ToDefaultAPIJson()
if err != nil {
panic(err) // cannot happen
}
w.WriteHeader(statuscode)
_, _ = w.Write([]byte(ginOutput))
}
+24 -21
View File
@@ -5,10 +5,10 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/bson/primitive"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"math" "math"
"strconv" "strconv"
"strings" "strings"
@@ -49,13 +49,13 @@ const (
type MetaValue struct { type MetaValue struct {
DataType metaDataType `json:"dataType"` DataType metaDataType `json:"dataType"`
Value interface{} `json:"value"` Value any `json:"value"`
} }
type metaValueSerialization struct { type metaValueSerialization struct {
DataType metaDataType `bson:"dataType"` DataType metaDataType `bson:"dataType"`
Value string `bson:"value"` Value string `bson:"value"`
Raw interface{} `bson:"raw"` Raw any `bson:"raw"`
} }
func (v MetaValue) SerializeValue() (string, error) { func (v MetaValue) SerializeValue() (string, error) {
@@ -379,7 +379,7 @@ func (v *MetaValue) Deserialize(value string, datatype metaDataType) error {
v.DataType = datatype v.DataType = datatype
return nil return nil
} else { } else {
v.Value = langext.Ptr(value[1:]) v.Value = new(value[1:])
v.DataType = datatype v.DataType = datatype
return nil return nil
} }
@@ -694,43 +694,46 @@ func (v MetaValue) rawValueForJson() any {
} }
func (mm MetaMap) FormatOneLine(singleMaxLen int) string { func (mm MetaMap) FormatOneLine(singleMaxLen int) string {
r := "" var r strings.Builder
i := 0 i := 0
for key, val := range mm { for key, val := range mm {
if i > 0 { if i > 0 {
r += ", " r.WriteString(", ")
} }
r += "\"" + key + "\"" r.WriteString("\"" + key + "\"")
r += ": " r.WriteString(": ")
r += "\"" + val.ShortString(singleMaxLen) + "\"" r.WriteString("\"" + val.ShortString(singleMaxLen) + "\"")
i++ i++
} }
return r return r.String()
} }
func (mm MetaMap) FormatMultiLine(indentFront string, indentKeys string, maxLenValue int) string { func (mm MetaMap) FormatMultiLine(indentFront string, indentKeys string, maxLenValue int) string {
r := "" var r strings.Builder
r += indentFront + "{" + "\n" r.WriteString(indentFront + "{" + "\n")
for key, val := range mm { for key, val := range mm {
if key == "gin.body" { if key == "gin.body" {
continue continue
} }
if key == "gin_body" {
continue
}
r += indentFront r.WriteString(indentFront)
r += indentKeys r.WriteString(indentKeys)
r += "\"" + key + "\"" r.WriteString("\"" + key + "\"")
r += ": " r.WriteString(": ")
r += "\"" + val.ShortString(maxLenValue) + "\"" r.WriteString("\"" + val.ShortString(maxLenValue) + "\"")
r += ",\n" r.WriteString(",\n")
} }
r += indentFront + "}" r.WriteString(indentFront + "}")
return r return r.String()
} }
func (mm MetaMap) Any() bool { func (mm MetaMap) Any() bool {
@@ -744,7 +747,7 @@ func (mm MetaMap) Apply(evt *zerolog.Event, limitLen *int) *zerolog.Event {
return evt return evt
} }
func (mm MetaMap) add(key string, mdtype metaDataType, val interface{}) { func (mm MetaMap) add(key string, mdtype metaDataType, val any) {
if _, ok := mm[key]; !ok { if _, ok := mm[key]; !ok {
mm[key] = MetaValue{DataType: mdtype, Value: val} mm[key] = MetaValue{DataType: mdtype, Value: val}
return return
+2 -2
View File
@@ -4,8 +4,8 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"gogs.mikescher.com/BlackForestBytes/goext/enums" "git.blackforestbytes.com/BlackForestBytes/goext/enums"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "git.blackforestbytes.com/BlackForestBytes/goext/langext"
"strings" "strings"
) )
+1 -1
View File
@@ -2,7 +2,7 @@ package ginext
import ( import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gogs.mikescher.com/BlackForestBytes/goext/dataext" "git.blackforestbytes.com/BlackForestBytes/goext/dataext"
) )
func BodyBuffer(g *gin.Context) { func BodyBuffer(g *gin.Context) {
+3 -3
View File
@@ -4,9 +4,9 @@ import (
"fmt" "fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "git.blackforestbytes.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/mathext" "git.blackforestbytes.com/BlackForestBytes/goext/mathext"
"gogs.mikescher.com/BlackForestBytes/goext/rext" "git.blackforestbytes.com/BlackForestBytes/goext/rext"
"net" "net"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
+61 -1
View File
@@ -2,8 +2,10 @@ package ginext
import ( import (
"fmt" "fmt"
"net/http"
"git.blackforestbytes.com/BlackForestBytes/goext/exerr"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gogs.mikescher.com/BlackForestBytes/goext/exerr"
) )
type WHandlerFunc func(PreContext) HTTPResponse type WHandlerFunc func(PreContext) HTTPResponse
@@ -39,6 +41,8 @@ func Wrap(w *GinWrapper, fn WHandlerFunc) gin.HandlerFunc {
} }
if g.Writer.Written() { if g.Writer.Written() {
// (!) This panic is not preoperly cached - it will kill the whole process
// but that only means you should NOT write in WrapperFunc...
panic("Writing in WrapperFunc is not supported") panic("Writing in WrapperFunc is not supported")
} }
@@ -53,8 +57,64 @@ func Wrap(w *GinWrapper, fn WHandlerFunc) gin.HandlerFunc {
lstr(g, wrap) lstr(g, wrap)
} }
if g.IsAborted() {
return
}
if reqctx.Err() == nil { if reqctx.Err() == nil {
wrap.Write(g) wrap.Write(g)
if g.IsAborted() {
// can happen if we try to marshal an invalid json object (e.g. containing Inf)
exerr.
New(exerr.TypePanic, "Failed to write response to renderer").
Strs("errors", g.Errors.Errors()).
Stack().
Print(g)
g.Status(500)
}
}
}
}
func WrapHTTPHandler(w *GinWrapper, fn http.Handler) gin.HandlerFunc {
return func(g *gin.Context) {
for _, lstr := range w.listenerBeforeRequest {
lstr(g)
}
fn.ServeHTTP(g.Writer, g.Request)
for _, lstr := range w.listenerAfterRequest {
lstr(g, nil)
}
}
}
func WrapHTTPHandlerFunc(w *GinWrapper, fn http.HandlerFunc) gin.HandlerFunc {
return func(g *gin.Context) {
for _, lstr := range w.listenerBeforeRequest {
lstr(g)
}
fn(g.Writer, g.Request)
for _, lstr := range w.listenerAfterRequest {
lstr(g, nil)
}
}
}
func WrapExtHTTPHandlerFunc(w *GinWrapper, fn func(*gin.Context, http.ResponseWriter, *http.Request)) gin.HandlerFunc {
return func(g *gin.Context) {
for _, lstr := range w.listenerBeforeRequest {
lstr(g)
}
fn(g, g.Writer, g.Request)
for _, lstr := range w.listenerAfterRequest {
lstr(g, nil)
} }
} }
} }
+15 -15
View File
@@ -4,11 +4,11 @@ import (
"bytes" "bytes"
"context" "context"
"fmt" "fmt"
"git.blackforestbytes.com/BlackForestBytes/goext/dataext"
"git.blackforestbytes.com/BlackForestBytes/goext/exerr"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding" "github.com/gin-gonic/gin/binding"
"gogs.mikescher.com/BlackForestBytes/goext/dataext"
"gogs.mikescher.com/BlackForestBytes/goext/exerr"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"io" "io"
"runtime/debug" "runtime/debug"
"time" "time"
@@ -84,7 +84,7 @@ func (pctx PreContext) Start() (*AppContext, *gin.Context, *HTTPResponse) {
WithType(exerr.TypeBindFailURI). WithType(exerr.TypeBindFailURI).
Str("struct_type", fmt.Sprintf("%T", pctx.uri)). Str("struct_type", fmt.Sprintf("%T", pctx.uri)).
Build() Build()
return CreateBackgroundAppContext(), nil, langext.Ptr(pctx.wrapper.buildRequestBindError(pctx.ginCtx, "URI", err)) return CreateBackgroundAppContext(), nil, new(pctx.wrapper.buildRequestBindError(pctx.ginCtx, "URI", err))
} }
} }
@@ -94,7 +94,7 @@ func (pctx PreContext) Start() (*AppContext, *gin.Context, *HTTPResponse) {
WithType(exerr.TypeBindFailQuery). WithType(exerr.TypeBindFailQuery).
Str("struct_type", fmt.Sprintf("%T", pctx.query)). Str("struct_type", fmt.Sprintf("%T", pctx.query)).
Build() Build()
return CreateBackgroundAppContext(), nil, langext.Ptr(pctx.wrapper.buildRequestBindError(pctx.ginCtx, "QUERY", err)) return CreateBackgroundAppContext(), nil, new(pctx.wrapper.buildRequestBindError(pctx.ginCtx, "QUERY", err))
} }
} }
@@ -108,7 +108,7 @@ func (pctx PreContext) Start() (*AppContext, *gin.Context, *HTTPResponse) {
WithType(exerr.TypeBindFailJSON). WithType(exerr.TypeBindFailJSON).
Str("struct_type", fmt.Sprintf("%T", pctx.body)). Str("struct_type", fmt.Sprintf("%T", pctx.body)).
Build() Build()
return CreateBackgroundAppContext(), nil, langext.Ptr(pctx.wrapper.buildRequestBindError(pctx.ginCtx, "JSON", err)) return CreateBackgroundAppContext(), nil, new(pctx.wrapper.buildRequestBindError(pctx.ginCtx, "JSON", err))
} }
} }
if err := pctx.ginCtx.ShouldBindJSON(pctx.body); err != nil { if err := pctx.ginCtx.ShouldBindJSON(pctx.body); err != nil {
@@ -116,14 +116,14 @@ func (pctx PreContext) Start() (*AppContext, *gin.Context, *HTTPResponse) {
WithType(exerr.TypeBindFailJSON). WithType(exerr.TypeBindFailJSON).
Str("struct_type", fmt.Sprintf("%T", pctx.body)). Str("struct_type", fmt.Sprintf("%T", pctx.body)).
Build() Build()
return CreateBackgroundAppContext(), nil, langext.Ptr(pctx.wrapper.buildRequestBindError(pctx.ginCtx, "JSON", err)) return CreateBackgroundAppContext(), nil, new(pctx.wrapper.buildRequestBindError(pctx.ginCtx, "JSON", err))
} }
} else { } else {
if !pctx.ignoreWrongContentType { if !pctx.ignoreWrongContentType {
err := exerr.New(exerr.TypeBindFailJSON, "missing JSON body"). err := exerr.New(exerr.TypeBindFailJSON, "missing JSON body").
Str("struct_type", fmt.Sprintf("%T", pctx.body)). Str("struct_type", fmt.Sprintf("%T", pctx.body)).
Build() Build()
return CreateBackgroundAppContext(), nil, langext.Ptr(pctx.wrapper.buildRequestBindError(pctx.ginCtx, "JSON", err)) return CreateBackgroundAppContext(), nil, new(pctx.wrapper.buildRequestBindError(pctx.ginCtx, "JSON", err))
} }
} }
} }
@@ -132,14 +132,14 @@ func (pctx PreContext) Start() (*AppContext, *gin.Context, *HTTPResponse) {
if brc, ok := pctx.ginCtx.Request.Body.(dataext.BufferedReadCloser); ok { if brc, ok := pctx.ginCtx.Request.Body.(dataext.BufferedReadCloser); ok {
v, err := brc.BufferedAll() v, err := brc.BufferedAll()
if err != nil { if err != nil {
return CreateBackgroundAppContext(), nil, langext.Ptr(pctx.wrapper.buildRequestBindError(pctx.ginCtx, "BODY", err)) return CreateBackgroundAppContext(), nil, new(pctx.wrapper.buildRequestBindError(pctx.ginCtx, "BODY", err))
} }
*pctx.rawbody = v *pctx.rawbody = v
} else { } else {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
_, err := io.Copy(buf, pctx.ginCtx.Request.Body) _, err := io.Copy(buf, pctx.ginCtx.Request.Body)
if err != nil { if err != nil {
return CreateBackgroundAppContext(), nil, langext.Ptr(pctx.wrapper.buildRequestBindError(pctx.ginCtx, "BODY", err)) return CreateBackgroundAppContext(), nil, new(pctx.wrapper.buildRequestBindError(pctx.ginCtx, "BODY", err))
} }
*pctx.rawbody = buf.Bytes() *pctx.rawbody = buf.Bytes()
} }
@@ -152,7 +152,7 @@ func (pctx PreContext) Start() (*AppContext, *gin.Context, *HTTPResponse) {
WithType(exerr.TypeBindFailFormData). WithType(exerr.TypeBindFailFormData).
Str("struct_type", fmt.Sprintf("%T", pctx.form)). Str("struct_type", fmt.Sprintf("%T", pctx.form)).
Build() Build()
return CreateBackgroundAppContext(), nil, langext.Ptr(pctx.wrapper.buildRequestBindError(pctx.ginCtx, "FORM", err)) return CreateBackgroundAppContext(), nil, new(pctx.wrapper.buildRequestBindError(pctx.ginCtx, "FORM", err))
} }
} else if pctx.ginCtx.ContentType() == "application/x-www-form-urlencoded" { } else if pctx.ginCtx.ContentType() == "application/x-www-form-urlencoded" {
if err := pctx.ginCtx.ShouldBindWith(pctx.form, binding.Form); err != nil { if err := pctx.ginCtx.ShouldBindWith(pctx.form, binding.Form); err != nil {
@@ -160,14 +160,14 @@ func (pctx PreContext) Start() (*AppContext, *gin.Context, *HTTPResponse) {
WithType(exerr.TypeBindFailFormData). WithType(exerr.TypeBindFailFormData).
Str("struct_type", fmt.Sprintf("%T", pctx.form)). Str("struct_type", fmt.Sprintf("%T", pctx.form)).
Build() Build()
return CreateBackgroundAppContext(), nil, langext.Ptr(pctx.wrapper.buildRequestBindError(pctx.ginCtx, "FORM", err)) return CreateBackgroundAppContext(), nil, new(pctx.wrapper.buildRequestBindError(pctx.ginCtx, "FORM", err))
} }
} else { } else {
if !pctx.ignoreWrongContentType { if !pctx.ignoreWrongContentType {
err := exerr.New(exerr.TypeBindFailFormData, "missing form body"). err := exerr.New(exerr.TypeBindFailFormData, "missing form body").
Str("struct_type", fmt.Sprintf("%T", pctx.form)). Str("struct_type", fmt.Sprintf("%T", pctx.form)).
Build() Build()
return CreateBackgroundAppContext(), nil, langext.Ptr(pctx.wrapper.buildRequestBindError(pctx.ginCtx, "FORM", err)) return CreateBackgroundAppContext(), nil, new(pctx.wrapper.buildRequestBindError(pctx.ginCtx, "FORM", err))
} }
} }
} }
@@ -178,7 +178,7 @@ func (pctx PreContext) Start() (*AppContext, *gin.Context, *HTTPResponse) {
WithType(exerr.TypeBindFailHeader). WithType(exerr.TypeBindFailHeader).
Str("struct_type", fmt.Sprintf("%T", pctx.query)). Str("struct_type", fmt.Sprintf("%T", pctx.query)).
Build() Build()
return CreateBackgroundAppContext(), nil, langext.Ptr(pctx.wrapper.buildRequestBindError(pctx.ginCtx, "HEADER", err)) return CreateBackgroundAppContext(), nil, new(pctx.wrapper.buildRequestBindError(pctx.ginCtx, "HEADER", err))
} }
} }
@@ -190,7 +190,7 @@ func (pctx PreContext) Start() (*AppContext, *gin.Context, *HTTPResponse) {
err := pctx.persistantData.sessionObj.Init(pctx.ginCtx, actx) err := pctx.persistantData.sessionObj.Init(pctx.ginCtx, actx)
if err != nil { if err != nil {
actx.Cancel() actx.Cancel()
return CreateBackgroundAppContext(), nil, langext.Ptr(pctx.wrapper.buildRequestBindError(pctx.ginCtx, "INIT", err)) return CreateBackgroundAppContext(), nil, new(pctx.wrapper.buildRequestBindError(pctx.ginCtx, "INIT", err))
} }
} }
+1 -1
View File
@@ -2,7 +2,7 @@ package ginext
import ( import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gogs.mikescher.com/BlackForestBytes/goext/exerr" "git.blackforestbytes.com/BlackForestBytes/goext/exerr"
) )
type cookieval struct { type cookieval struct {
+2 -2
View File
@@ -1,8 +1,8 @@
package ginext package ginext
import ( import (
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
) )
type dataHTTPResponse struct { type dataHTTPResponse struct {
@@ -42,7 +42,7 @@ func (j dataHTTPResponse) Statuscode() int {
} }
func (j dataHTTPResponse) BodyString(*gin.Context) *string { func (j dataHTTPResponse) BodyString(*gin.Context) *string {
return langext.Ptr(string(j.data)) return new(string(j.data))
} }
func (j dataHTTPResponse) ContentType() string { func (j dataHTTPResponse) ContentType() string {
+2 -2
View File
@@ -2,8 +2,8 @@ package ginext
import ( import (
"fmt" "fmt"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
) )
type downloadDataHTTPResponse struct { type downloadDataHTTPResponse struct {
@@ -48,7 +48,7 @@ func (j downloadDataHTTPResponse) Statuscode() int {
} }
func (j downloadDataHTTPResponse) BodyString(*gin.Context) *string { func (j downloadDataHTTPResponse) BodyString(*gin.Context) *string {
return langext.Ptr(string(j.data)) return new(string(j.data))
} }
func (j downloadDataHTTPResponse) ContentType() string { func (j downloadDataHTTPResponse) ContentType() string {
+1 -1
View File
@@ -2,7 +2,7 @@ package ginext
import ( import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "git.blackforestbytes.com/BlackForestBytes/goext/langext"
) )
type emptyHTTPResponse struct { type emptyHTTPResponse struct {
+2 -2
View File
@@ -2,8 +2,8 @@ package ginext
import ( import (
"fmt" "fmt"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"os" "os"
) )
@@ -53,7 +53,7 @@ func (j fileHTTPResponse) BodyString(*gin.Context) *string {
if err != nil { if err != nil {
return nil return nil
} }
return langext.Ptr(string(data)) return new(string(data))
} }
func (j fileHTTPResponse) ContentType() string { func (j fileHTTPResponse) ContentType() string {
+2 -2
View File
@@ -2,8 +2,8 @@ package ginext
import ( import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
json "gogs.mikescher.com/BlackForestBytes/goext/gojson" json "git.blackforestbytes.com/BlackForestBytes/goext/gojson"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "git.blackforestbytes.com/BlackForestBytes/goext/langext"
) )
type jsonHTTPResponse struct { type jsonHTTPResponse struct {
+2 -2
View File
@@ -3,8 +3,8 @@ package ginext
import ( import (
"context" "context"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gogs.mikescher.com/BlackForestBytes/goext/exerr" "git.blackforestbytes.com/BlackForestBytes/goext/exerr"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "git.blackforestbytes.com/BlackForestBytes/goext/langext"
) )
type jsonAPIErrResponse struct { type jsonAPIErrResponse struct {
+1 -1
View File
@@ -2,7 +2,7 @@ package ginext
import ( import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "git.blackforestbytes.com/BlackForestBytes/goext/langext"
) )
type redirectHTTPResponse struct { type redirectHTTPResponse struct {
+3 -3
View File
@@ -1,9 +1,9 @@
package ginext package ginext
import ( import (
"git.blackforestbytes.com/BlackForestBytes/goext/exerr"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gogs.mikescher.com/BlackForestBytes/goext/exerr"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"io" "io"
"net/http" "net/http"
"time" "time"
@@ -56,7 +56,7 @@ func (j seekableResponse) Statuscode() int {
} }
func (j seekableResponse) BodyString(*gin.Context) *string { func (j seekableResponse) BodyString(*gin.Context) *string {
return langext.Ptr("(seekable)") return new("(seekable)")
} }
func (j seekableResponse) ContentType() string { func (j seekableResponse) ContentType() string {
+2 -2
View File
@@ -1,8 +1,8 @@
package ginext package ginext
import ( import (
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
) )
type textHTTPResponse struct { type textHTTPResponse struct {
@@ -41,7 +41,7 @@ func (j textHTTPResponse) Statuscode() int {
} }
func (j textHTTPResponse) BodyString(*gin.Context) *string { func (j textHTTPResponse) BodyString(*gin.Context) *string {
return langext.Ptr(j.data) return new(j.data)
} }
func (j textHTTPResponse) ContentType() string { func (j textHTTPResponse) ContentType() string {
+96 -2
View File
@@ -1,13 +1,14 @@
package ginext package ginext
import ( import (
"github.com/gin-gonic/gin"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"net/http" "net/http"
"path" "path"
"reflect" "reflect"
"runtime" "runtime"
"strings" "strings"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
"github.com/gin-gonic/gin"
) )
var anyMethods = []string{ var anyMethods = []string{
@@ -148,6 +149,99 @@ func (w *GinRouteBuilder) Handle(handler WHandlerFunc) {
}) })
} }
func (w *GinRouteBuilder) HandleRawHTTPHandler(f http.Handler) {
if w.routes.wrapper.bufferBody {
arr := make([]gin.HandlerFunc, 0, len(w.handlers)+1)
arr = append(arr, BodyBuffer)
arr = append(arr, w.handlers...)
w.handlers = arr
}
middlewareNames := langext.ArrMap(w.handlers, func(v gin.HandlerFunc) string { return nameOfFunction(v) })
w.handlers = append(w.handlers, WrapHTTPHandler(w.routes.wrapper, f))
methodName := w.method
if w.method == "*" {
methodName = "ANY"
for _, method := range anyMethods {
w.routes.routes.Handle(method, w.relPath, w.handlers...)
}
} else {
w.routes.routes.Handle(w.method, w.relPath, w.handlers...)
}
w.routes.wrapper.routeSpecs = append(w.routes.wrapper.routeSpecs, ginRouteSpec{
Method: methodName,
URL: w.absPath,
Middlewares: middlewareNames,
Handler: "[HTTPHandler]",
})
}
func (w *GinRouteBuilder) HandleRawHTTPHandlerFunc(f http.HandlerFunc) {
if w.routes.wrapper.bufferBody {
arr := make([]gin.HandlerFunc, 0, len(w.handlers)+1)
arr = append(arr, BodyBuffer)
arr = append(arr, w.handlers...)
w.handlers = arr
}
middlewareNames := langext.ArrMap(w.handlers, func(v gin.HandlerFunc) string { return nameOfFunction(v) })
w.handlers = append(w.handlers, WrapHTTPHandlerFunc(w.routes.wrapper, f))
methodName := w.method
if w.method == "*" {
methodName = "ANY"
for _, method := range anyMethods {
w.routes.routes.Handle(method, w.relPath, w.handlers...)
}
} else {
w.routes.routes.Handle(w.method, w.relPath, w.handlers...)
}
w.routes.wrapper.routeSpecs = append(w.routes.wrapper.routeSpecs, ginRouteSpec{
Method: methodName,
URL: w.absPath,
Middlewares: middlewareNames,
Handler: "[HTTPHandlerFunc]",
})
}
func (w *GinRouteBuilder) HandleExtHTTPHandlerFunc(f func(*gin.Context, http.ResponseWriter, *http.Request)) {
if w.routes.wrapper.bufferBody {
arr := make([]gin.HandlerFunc, 0, len(w.handlers)+1)
arr = append(arr, BodyBuffer)
arr = append(arr, w.handlers...)
w.handlers = arr
}
middlewareNames := langext.ArrMap(w.handlers, func(v gin.HandlerFunc) string { return nameOfFunction(v) })
w.handlers = append(w.handlers, WrapExtHTTPHandlerFunc(w.routes.wrapper, f))
methodName := w.method
if w.method == "*" {
methodName = "ANY"
for _, method := range anyMethods {
w.routes.routes.Handle(method, w.relPath, w.handlers...)
}
} else {
w.routes.routes.Handle(w.method, w.relPath, w.handlers...)
}
w.routes.wrapper.routeSpecs = append(w.routes.wrapper.routeSpecs, ginRouteSpec{
Method: methodName,
URL: w.absPath,
Middlewares: middlewareNames,
Handler: "[HTTPHandlerFunc]",
})
}
func (w *GinWrapper) NoRoute(handler WHandlerFunc) { func (w *GinWrapper) NoRoute(handler WHandlerFunc) {
handlers := make([]gin.HandlerFunc, 0) handlers := make([]gin.HandlerFunc, 0)
+48 -37
View File
@@ -1,66 +1,77 @@
module gogs.mikescher.com/BlackForestBytes/goext module git.blackforestbytes.com/BlackForestBytes/goext
go 1.23.0 go 1.26.0
toolchain go1.24.0
require ( require (
github.com/gin-gonic/gin v1.10.0 github.com/gin-gonic/gin v1.12.0
github.com/glebarez/go-sqlite v1.22.0 // only needed for tests -.- github.com/glebarez/go-sqlite v1.22.0 // only needed for tests -.-
github.com/jmoiron/sqlx v1.4.0 github.com/jmoiron/sqlx v1.4.0
github.com/rs/xid v1.6.0 github.com/rs/xid v1.6.0
github.com/rs/zerolog v1.34.0 github.com/rs/zerolog v1.35.1
go.mongodb.org/mongo-driver v1.17.3 go.mongodb.org/mongo-driver v1.17.9
golang.org/x/crypto v0.37.0 golang.org/x/crypto v0.50.0
golang.org/x/sys v0.32.0 golang.org/x/sys v0.43.0
golang.org/x/term v0.31.0 golang.org/x/term v0.42.0
) )
require ( require (
github.com/360EntSecGroup-Skylar/excelize v1.4.1
github.com/disintegration/imaging v1.6.2 github.com/disintegration/imaging v1.6.2
github.com/gorilla/websocket v1.5.3
github.com/jung-kurt/gofpdf v1.16.2 github.com/jung-kurt/gofpdf v1.16.2
golang.org/x/sync v0.13.0 github.com/xuri/excelize/v2 v2.10.1
golang.org/x/sync v0.20.0
) )
require ( require (
github.com/bytedance/sonic v1.13.2 // indirect github.com/bytedance/gopkg v0.1.4 // indirect
github.com/bytedance/sonic/loader v0.2.4 // indirect github.com/bytedance/sonic v1.15.0 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect github.com/bytedance/sonic/loader v0.5.1 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.9 // indirect github.com/gabriel-vasile/mimetype v1.4.13 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect github.com/gin-contrib/sse v1.1.1 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.26.0 // indirect github.com/go-playground/validator/v10 v10.30.2 // indirect
github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-json v0.10.6 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect
github.com/golang/snappy v1.0.0 // indirect github.com/golang/snappy v1.0.0 // indirect
github.com/google/uuid v1.5.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/compress v1.18.5 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.21 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/montanaflynn/stats v0.7.1 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/montanaflynn/stats v0.9.0 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/pelletier/go-toml/v2 v2.3.0 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.59.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/richardlehane/mscfb v1.0.6 // indirect
github.com/richardlehane/msoleps v1.0.6 // indirect
github.com/tiendc/go-deepcopy v1.7.2 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect github.com/ugorji/go/codec v1.3.1 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect github.com/xdg-go/scram v1.2.0 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/xuri/efp v0.0.1 // indirect
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
golang.org/x/arch v0.16.0 // indirect go.mongodb.org/mongo-driver/v2 v2.5.1 // indirect
golang.org/x/image v0.26.0 // indirect golang.org/x/arch v0.26.0 // indirect
golang.org/x/net v0.39.0 // indirect golang.org/x/image v0.39.0 // indirect
golang.org/x/text v0.24.0 // indirect golang.org/x/net v0.53.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect golang.org/x/text v0.36.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect google.golang.org/protobuf v1.36.11 // indirect
modernc.org/libc v1.37.6 // indirect modernc.org/libc v1.72.0 // indirect
modernc.org/mathutil v1.6.0 // indirect modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.7.2 // indirect modernc.org/memory v1.11.0 // indirect
modernc.org/sqlite v1.28.0 // indirect modernc.org/sqlite v1.49.1 // indirect
) )
+127 -269
View File
@@ -1,44 +1,16 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/360EntSecGroup-Skylar/excelize v1.4.1 h1:l55mJb6rkkaUzOpSsgEeKYtS6/0gHwBYyfo5Jcjv/Ks=
github.com/360EntSecGroup-Skylar/excelize v1.4.1/go.mod h1:vnax29X2usfl7HHkBrX5EvSCJcmH3dT9luvxzu8iGAE=
github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/bytedance/sonic v1.12.3 h1:W2MGa7RCU1QTeYRTPE3+88mVC0yXmsRQRChiyVocVjU= github.com/bytedance/gopkg v0.1.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM=
github.com/bytedance/sonic v1.12.3/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk= github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4=
github.com/bytedance/sonic v1.12.4 h1:9Csb3c9ZJhfUWeMtpCDCq6BUoH5ogfDFLUgQ/jG+R0k= github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
github.com/bytedance/sonic v1.12.4/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk= github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
github.com/bytedance/sonic v1.12.5 h1:hoZxY8uW+mT+OpkcUWw4k0fDINtOcVavEsGfzwzFU/w= github.com/bytedance/sonic/loader v0.5.1 h1:Ygpfa9zwRCCKSlrp5bBP/b/Xzc3VxsAW+5NIYXrOOpI=
github.com/bytedance/sonic v1.12.5/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk= github.com/bytedance/sonic/loader v0.5.1/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/bytedance/sonic v1.12.6 h1:/isNmCUF2x3Sh8RAp/4mh4ZGkcFAX/hLrzrK3AvpRzk= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/bytedance/sonic v1.12.6/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/bytedance/sonic v1.12.7 h1:CQU8pxOy9HToxhndH0Kx/S1qU/CuS9GnKYrGioDcU1Q=
github.com/bytedance/sonic v1.12.7/go.mod h1:tnbal4mxOMju17EGfknm2XyYcpyCnIROYOEYuemj13I=
github.com/bytedance/sonic v1.12.8 h1:4xYRVRlXIgvSZ4e8iVTlMF5szgpXd4AfvuWgA8I8lgs=
github.com/bytedance/sonic v1.12.8/go.mod h1:uVvFidNmlt9+wa31S1urfwwthTWteBgG0hWuoKAXTx8=
github.com/bytedance/sonic v1.12.9 h1:Od1BvK55NnewtGaJsTDeAOSnLVO2BTSLOe0+ooKokmQ=
github.com/bytedance/sonic v1.12.9/go.mod h1:uVvFidNmlt9+wa31S1urfwwthTWteBgG0hWuoKAXTx8=
github.com/bytedance/sonic v1.12.10 h1:uVCQr6oS5669E9ZVW0HyksTLfNS7Q/9hV6IVS4nEMsI=
github.com/bytedance/sonic v1.12.10/go.mod h1:uVvFidNmlt9+wa31S1urfwwthTWteBgG0hWuoKAXTx8=
github.com/bytedance/sonic v1.13.1 h1:Jyd5CIvdFnkOWuKXr+wm4Nyk2h0yAFsr8ucJgEasO3g=
github.com/bytedance/sonic v1.13.1/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP1iU4kWM=
github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.1 h1:1GgorWTqf12TA8mma4DDSbaQigE2wOgQo7iCjjJv3+E=
github.com/bytedance/sonic/loader v0.2.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.2 h1:jxAJuN9fOot/cyz5Q6dUuMJF5OqQ6+5GfA8FjjQ0R4o=
github.com/bytedance/sonic/loader v0.2.2/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/bytedance/sonic/loader v0.2.3 h1:yctD0Q3v2NOGfSWPLPvG2ggA2kV6TS6s4wioyEqssH0=
github.com/bytedance/sonic/loader v0.2.3/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -46,24 +18,12 @@ github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/gabriel-vasile/mimetype v1.4.5 h1:J7wGKdGu33ocBOhGy0z653k/lFKLFDPJMG8Gql0kxn4= github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
github.com/gabriel-vasile/mimetype v1.4.5/go.mod h1:ibHel+/kbxn9x2407k1izTA1S81ku1z/DlgOW2QE0M4= github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gabriel-vasile/mimetype v1.4.6 h1:3+PzJTKLkvgjeTbts6msPJt4DixhT4YtFNf1gtGe3zc= github.com/gin-contrib/sse v1.1.1 h1:uGYpNwTacv5R68bSGMapo62iLTRa9l5zxGCps4hK6ko=
github.com/gabriel-vasile/mimetype v1.4.6/go.mod h1:JX1qVKqZd40hUPpAfiNTe0Sne7hdfKSbOqqmkq8GCXc= github.com/gin-contrib/sse v1.1.1/go.mod h1:QXzuVkA0YO7o/gun03UI1Q+FTI8ZV/n5t03kIQAI89s=
github.com/gabriel-vasile/mimetype v1.4.7 h1:SKFKl7kD0RiPdbht0s7hFtjl489WcQ1VyPW8ZzUMYCA= github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
github.com/gabriel-vasile/mimetype v1.4.7/go.mod h1:GDlAgAyIRT27BhFl53XNAFtfjzOkLaF35JdEG0P7LtU= github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E=
github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ= github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc= github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
@@ -72,36 +32,27 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA= github.com/go-playground/validator/v10 v10.30.2 h1:JiFIMtSSHb2/XBUbWM4i/MpeQm9ZK2xqPNk8vgvu5JQ=
github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-playground/validator/v10 v10.30.2/go.mod h1:mAf2pIOVXjTEBrwUMGKkCWKKPs9NheYGabeB04txQSc=
github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o=
github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-playground/validator/v10 v10.24.0 h1:KHQckvo8G6hlWnrPX4NJJ+aBfWNAE/HH+qdL2cBpCmg=
github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus=
github.com/go-playground/validator/v10 v10.25.0 h1:5Dh7cjvzR7BRZadnsVOzPhWsrwUr0nmsZJxEAnFLNO8=
github.com/go-playground/validator/v10 v10.25.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus=
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
@@ -109,32 +60,18 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm
github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
github.com/jung-kurt/gofpdf v1.16.2 h1:jgbatWHfRlPYiK85qgevsZTHviWXKwB1TTiKdz5PtRc= github.com/jung-kurt/gofpdf v1.16.2 h1:jgbatWHfRlPYiK85qgevsZTHviWXKwB1TTiKdz5PtRc=
github.com/jung-kurt/gofpdf v1.16.2/go.mod h1:1hl7y57EsiPAkLbOwzpzqgx1A30nQCk/YmFV8S2vmK0= github.com/jung-kurt/gofpdf v1.16.2/go.mod h1:1hl7y57EsiPAkLbOwzpzqgx1A30nQCk/YmFV8S2vmK0=
github.com/klauspost/compress v1.17.10 h1:oXAz+Vh0PMUvJczoi+flxpnBEPxoER1IaAnU/NMPtT0= github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
github.com/klauspost/compress v1.17.10/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -142,228 +79,149 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= github.com/montanaflynn/stats v0.9.0 h1:tsBJ0RXwph9BmAuFoCmqGv6e8xa0MENQ8m0ptKq29mQ=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= github.com/montanaflynn/stats v0.9.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM=
github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/phpdave11/gofpdi v1.0.7/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= github.com/phpdave11/gofpdi v1.0.7/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/richardlehane/mscfb v1.0.6 h1:eN3bvvZCp00bs7Zf52bxNwAx5lJDBK1tCuH19qq5aC8=
github.com/richardlehane/mscfb v1.0.6/go.mod h1:pe0+IUIc0AHh0+teNzBlJCtSyZdFOGgV4ZK9bsoV+Jo=
github.com/richardlehane/msoleps v1.0.6 h1:9BvkpjvD+iUBalUY4esMwv6uBkfOip/Lzvd93jvR9gg=
github.com/richardlehane/msoleps v1.0.6/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI=
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.3-0.20181224173747-660f15d67dbb/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tiendc/go-deepcopy v1.7.2 h1:Ut2yYR7W9tWjTQitganoIue4UGxZwCcJy3orjrrIj44=
github.com/tiendc/go-deepcopy v1.7.2/go.mod h1:4bKjNC2r7boYOkD2IOuZpYjmlDdzjbpTRyCx+goBCJQ=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= github.com/xdg-go/scram v1.2.0 h1:bYKF2AEwG5rqd1BumT4gAnvwU/M9nBp2pTSxeZw7Wvs=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= github.com/xdg-go/scram v1.2.0/go.mod h1:3dlrS0iBaWKYVt2ZfA4cj48umJZ+cAEbR6/SjLA88I8=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8=
github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
github.com/xuri/excelize/v2 v2.10.1 h1:V62UlqopMqha3kOpnlHy2CcRVw1V8E63jFoWUmMzxN0=
github.com/xuri/excelize/v2 v2.10.1/go.mod h1:iG5tARpgaEeIhTqt3/fgXCGoBRt4hNXgCp3tfXKoOIc=
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBLbf3WdLgC29pgyhTjAT/0nuE=
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.mongodb.org/mongo-driver v1.17.1 h1:Wic5cJIwJgSpBhe3lx3+/RybR5PiYRMpVFgO7cOHyIM= go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU=
go.mongodb.org/mongo-driver v1.17.1/go.mod h1:wwWm/+BuOddhcq3n68LKRmgk2wXzmF6s0SFOa0GINL4= go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ=
go.mongodb.org/mongo-driver v1.17.2 h1:gvZyk8352qSfzyZ2UMWcpDpMSGEr1eqE4T793SqyhzM= go.mongodb.org/mongo-driver/v2 v2.5.1 h1:j2U/Qp+wvueSpqitLCSZPT/+ZpVc1xzuwdHWwl7d8ro=
go.mongodb.org/mongo-driver v1.17.2/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= go.mongodb.org/mongo-driver/v2 v2.5.1/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
go.mongodb.org/mongo-driver v1.17.3 h1:TQyXhnsWfWtgAhMtOgtYHMTkZIfBTpMTsMnd9ZBeHxQ= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.mongodb.org/mongo-driver v1.17.3/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
golang.org/x/arch v0.11.0 h1:KXV8WWKCXm6tRpLirl2szsO5j/oOODwZf4hATmGVNs4= golang.org/x/arch v0.26.0 h1:jZ6dpec5haP/fUv1kLCbuJy6dnRrfX6iVK08lZBFpk4=
golang.org/x/arch v0.11.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/arch v0.26.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8=
golang.org/x/arch v0.12.0 h1:UsYJhbzPYGsT0HbEdmYcqtCv8UNGvnaL561NnIUvaKg=
golang.org/x/arch v0.12.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/arch v0.13.0 h1:KCkqVVV1kGg0X87TFysjCJ8MxtZEIU4Ja/yXGeoECdA=
golang.org/x/arch v0.13.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/arch v0.14.0 h1:z9JUEZWr8x4rR0OU6c4/4t6E6jOZ8/QBS2bBYBm4tx4=
golang.org/x/arch v0.14.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw=
golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
golang.org/x/arch v0.16.0 h1:foMtLTdyOmIniqWCHjY6+JxuC54XP1fDwx4N0ASyW+U=
golang.org/x/arch v0.16.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY=
golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.21.0 h1:c5qV36ajHpdj4Qi0GnE0jUc/yuo33OLFaa0d+crTD5s= golang.org/x/image v0.39.0 h1:skVYidAEVKgn8lZ602XO75asgXBgLj9G/FE3RbuPFww=
golang.org/x/image v0.21.0/go.mod h1:vUbsLavqK/W303ZroQQVKQ+Af3Yl6Uz1Ppu5J/cLz78= golang.org/x/image v0.39.0/go.mod h1:sIbmppfU+xFLPIG0FoVUTvyBMmgng1/XAMhQ2ft0hpA=
golang.org/x/image v0.22.0 h1:UtK5yLUzilVrkjMAZAZ34DXGpASN8i8pj8g+O+yd10g=
golang.org/x/image v0.22.0/go.mod h1:9hPFhljd4zZ1GNSIZJ49sqbp45GKK9t6w+iXvGqZUz4=
golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68=
golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY=
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=
golang.org/x/image v0.26.0 h1:4XjIFEZWQmCZi6Wv8BoxsDhRU3RVnLX04dToTDAEPlY=
golang.org/x/image v0.26.0/go.mod h1:lcxbMFAovzpnJxzXS3nyL83K27tmqtKzIJpctK8YO5c=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI=
golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io=
google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.2 h1:R8FeyR1/eLmkutZOM5CWghmo5itiG9z0ktFlTVLuTmU=
google.golang.org/protobuf v1.36.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
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/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= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/libc v1.37.6 h1:orZH3c5wmhIQFTXF+Nt+eeauyd+ZIt2BX6ARe+kD+aw= modernc.org/cc/v4 v4.27.3 h1:uNCgn37E5U09mTv1XgskEVUJ8ADKpmFMPxzGJ0TSo+U=
modernc.org/libc v1.37.6/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE= modernc.org/cc/v4 v4.27.3/go.mod h1:3YjcbCqhoTTHPycJDRl2WZKKFj0nwcOIPBfEZK0Hdk8=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= modernc.org/ccgo/v4 v4.32.4 h1:L5OB8rpEX4ZsXEQwGozRfJyJSFHbbNVOoQ59DU9/KuU=
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= modernc.org/ccgo/v4 v4.32.4/go.mod h1:lY7f+fiTDHfcv6YlRgSkxYfhs+UvOEEzj49jAn2TOx0=
modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E= modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E= modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0= modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.72.0 h1:IEu559v9a0XWjw0DPoVKtXpO2qt5NVLAnFaBbjq+n8c=
modernc.org/libc v1.72.0/go.mod h1:tTU8DL8A+XLVkEY3x5E/tO7s2Q/q42EtnNWda/L5QhQ=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.49.1 h1:dYGHTKcX1sJ+EQDnUzvz4TJ5GbuvhNJa8Fg6ElGx73U=
modernc.org/sqlite v1.49.1/go.mod h1:m0w8xhwYUVY3H6pSDwc3gkJ/irZT/0YEXwBlhaxQEew=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
+2 -2
View File
@@ -1,5 +1,5 @@
package goext package goext
const GoextVersion = "0.0.571" const GoextVersion = "0.0.633"
const GoextVersionTimestamp = "2025-04-30T21:06:24+0200" const GoextVersionTimestamp = "2026-04-13T16:12:09+0200"
+8 -8
View File
@@ -108,11 +108,11 @@ func (u unmarshalerText) MarshalText() ([]byte, error) {
} }
func (u *unmarshalerText) UnmarshalText(b []byte) error { func (u *unmarshalerText) UnmarshalText(b []byte) error {
pos := bytes.IndexByte(b, ':') before, after, ok := bytes.Cut(b, []byte{':'})
if pos == -1 { if !ok {
return errors.New("missing separator") return errors.New("missing separator")
} }
u.A, u.B = string(b[:pos]), string(b[pos+1:]) u.A, u.B = string(before), string(after)
return nil return nil
} }
@@ -126,7 +126,7 @@ type ustructText struct {
type u8marshal uint8 type u8marshal uint8
func (u8 u8marshal) MarshalText() ([]byte, error) { func (u8 u8marshal) MarshalText() ([]byte, error) {
return []byte(fmt.Sprintf("u%d", u8)), nil return fmt.Appendf(nil, "u%d", u8), nil
} }
var errMissingU8Prefix = errors.New("missing 'u' prefix") var errMissingU8Prefix = errors.New("missing 'u' prefix")
@@ -275,7 +275,7 @@ func (unexportedWithMethods) F() {}
type byteWithMarshalJSON byte type byteWithMarshalJSON byte
func (b byteWithMarshalJSON) MarshalJSON() ([]byte, error) { func (b byteWithMarshalJSON) MarshalJSON() ([]byte, error) {
return []byte(fmt.Sprintf(`"Z%.2x"`, byte(b))), nil return fmt.Appendf(nil, `"Z%.2x"`, byte(b)), nil
} }
func (b *byteWithMarshalJSON) UnmarshalJSON(data []byte) error { func (b *byteWithMarshalJSON) UnmarshalJSON(data []byte) error {
@@ -303,7 +303,7 @@ func (b *byteWithPtrMarshalJSON) UnmarshalJSON(data []byte) error {
type byteWithMarshalText byte type byteWithMarshalText byte
func (b byteWithMarshalText) MarshalText() ([]byte, error) { func (b byteWithMarshalText) MarshalText() ([]byte, error) {
return []byte(fmt.Sprintf(`Z%.2x`, byte(b))), nil return fmt.Appendf(nil, `Z%.2x`, byte(b)), nil
} }
func (b *byteWithMarshalText) UnmarshalText(data []byte) error { func (b *byteWithMarshalText) UnmarshalText(data []byte) error {
@@ -331,7 +331,7 @@ func (b *byteWithPtrMarshalText) UnmarshalText(data []byte) error {
type intWithMarshalJSON int type intWithMarshalJSON int
func (b intWithMarshalJSON) MarshalJSON() ([]byte, error) { func (b intWithMarshalJSON) MarshalJSON() ([]byte, error) {
return []byte(fmt.Sprintf(`"Z%.2x"`, int(b))), nil return fmt.Appendf(nil, `"Z%.2x"`, int(b)), nil
} }
func (b *intWithMarshalJSON) UnmarshalJSON(data []byte) error { func (b *intWithMarshalJSON) UnmarshalJSON(data []byte) error {
@@ -359,7 +359,7 @@ func (b *intWithPtrMarshalJSON) UnmarshalJSON(data []byte) error {
type intWithMarshalText int type intWithMarshalText int
func (b intWithMarshalText) MarshalText() ([]byte, error) { func (b intWithMarshalText) MarshalText() ([]byte, error) {
return []byte(fmt.Sprintf(`Z%.2x`, int(b))), nil return fmt.Appendf(nil, `Z%.2x`, int(b)), nil
} }
func (b *intWithMarshalText) UnmarshalText(data []byte) error { func (b *intWithMarshalText) UnmarshalText(data []byte) error {
+5 -13
View File
@@ -177,7 +177,7 @@ type IndentOpt struct {
// MarshalSafeCollections is like Marshal except it will marshal nil maps and // MarshalSafeCollections is like Marshal except it will marshal nil maps and
// slices as '{}' and '[]' respectfully instead of 'null' // slices as '{}' and '[]' respectfully instead of 'null'
func MarshalSafeCollections(v interface{}, nilSafeSlices bool, nilSafeMaps bool, indent *IndentOpt, filter *string) ([]byte, error) { func MarshalSafeCollections(v any, nilSafeSlices bool, nilSafeMaps bool, indent *IndentOpt, filter *string) ([]byte, error) {
e := &encodeState{} e := &encodeState{}
err := e.marshal(v, encOpts{escapeHTML: true, nilSafeSlices: nilSafeSlices, nilSafeMaps: nilSafeMaps, filter: filter}) err := e.marshal(v, encOpts{escapeHTML: true, nilSafeSlices: nilSafeSlices, nilSafeMaps: nilSafeMaps, filter: filter})
if err != nil { if err != nil {
@@ -891,7 +891,7 @@ func (se sliceEncoder) encode(e *encodeState, v reflect.Value, opts encOpts) {
// Here we use a struct to memorize the pointer to the first element of the slice // Here we use a struct to memorize the pointer to the first element of the slice
// and its length. // and its length.
ptr := struct { ptr := struct {
ptr interface{} // always an unsafe.Pointer, but avoids a dependency on package unsafe ptr any // always an unsafe.Pointer, but avoids a dependency on package unsafe
len int len int
}{v.UnsafePointer(), v.Len()} }{v.UnsafePointer(), v.Len()}
if _, ok := e.ptrSeen[ptr]; ok { if _, ok := e.ptrSeen[ptr]; ok {
@@ -923,7 +923,7 @@ type arrayEncoder struct {
func (ae arrayEncoder) encode(e *encodeState, v reflect.Value, opts encOpts) { func (ae arrayEncoder) encode(e *encodeState, v reflect.Value, opts encOpts) {
e.WriteByte('[') e.WriteByte('[')
n := v.Len() n := v.Len()
for i := 0; i < n; i++ { for i := range n {
if i > 0 { if i > 0 {
e.WriteByte(',') e.WriteByte(',')
} }
@@ -1075,10 +1075,7 @@ func appendString[Bytes []byte | string](dst []byte, src Bytes, escapeHTML bool)
// For now, cast only a small portion of byte slices to a string // For now, cast only a small portion of byte slices to a string
// so that it can be stack allocated. This slows down []byte slightly // so that it can be stack allocated. This slows down []byte slightly
// due to the extra copy, but keeps string performance roughly the same. // due to the extra copy, but keeps string performance roughly the same.
n := len(src) - i n := min(len(src)-i, utf8.UTFMax)
if n > utf8.UTFMax {
n = utf8.UTFMax
}
c, size := utf8.DecodeRuneInString(string(src[i : i+n])) c, size := utf8.DecodeRuneInString(string(src[i : i+n]))
if c == utf8.RuneError && size == 1 { if c == utf8.RuneError && size == 1 {
dst = append(dst, src[start:i]...) dst = append(dst, src[start:i]...)
@@ -1130,12 +1127,7 @@ type field struct {
type jsonfilter []string type jsonfilter []string
func (j jsonfilter) Contains(t string) bool { func (j jsonfilter) Contains(t string) bool {
for _, tag := range j { return slices.Contains(j, t)
if t == tag {
return true
}
}
return false
} }
// typeFields returns a list of fields that JSON should recognize for the given type. // typeFields returns a list of fields that JSON should recognize for the given type.
+14 -15
View File
@@ -41,7 +41,7 @@ type Optionals struct {
Uo uint `json:"uo,omitempty"` Uo uint `json:"uo,omitempty"`
Str struct{} `json:"str"` Str struct{} `json:"str"`
Sto struct{} `json:"sto,omitempty"` Sto struct{} `json:"sto"`
} }
func TestOmitEmpty(t *testing.T) { func TestOmitEmpty(t *testing.T) {
@@ -1166,8 +1166,7 @@ func TestMarshalUncommonFieldNames(t *testing.T) {
} }
func TestMarshalerError(t *testing.T) { func TestMarshalerError(t *testing.T) {
s := "test variable" st := reflect.TypeFor[string]()
st := reflect.TypeOf(s)
const errText = "json: test error" const errText = "json: test error"
tests := []struct { tests := []struct {
@@ -1222,18 +1221,18 @@ func TestIssue63379(t *testing.T) {
func TestMarshalSafeCollections(t *testing.T) { func TestMarshalSafeCollections(t *testing.T) {
var ( var (
nilSlice []interface{} nilSlice []any
pNilSlice *[]interface{} pNilSlice *[]any
nilMap map[string]interface{} nilMap map[string]any
pNilMap *map[string]interface{} pNilMap *map[string]any
) )
type ( type (
nilSliceStruct struct { nilSliceStruct struct {
NilSlice []interface{} `json:"nil_slice"` NilSlice []any `json:"nil_slice"`
} }
nilMapStruct struct { nilMapStruct struct {
NilMap map[string]interface{} `json:"nil_map"` NilMap map[string]any `json:"nil_map"`
} }
testWithFilter struct { testWithFilter struct {
Test1 string `json:"test1" jsonfilter:"FILTERONE"` Test1 string `json:"test1" jsonfilter:"FILTERONE"`
@@ -1242,19 +1241,19 @@ func TestMarshalSafeCollections(t *testing.T) {
) )
tests := []struct { tests := []struct {
in interface{} in any
want string want string
}{ }{
{nilSlice, "[]"}, {nilSlice, "[]"},
{[]interface{}{}, "[]"}, {[]any{}, "[]"},
{make([]interface{}, 0), "[]"}, {make([]any, 0), "[]"},
{[]int{1, 2, 3}, "[1,2,3]"}, {[]int{1, 2, 3}, "[1,2,3]"},
{pNilSlice, "null"}, {pNilSlice, "null"},
{nilSliceStruct{}, "{\"nil_slice\":[]}"}, {nilSliceStruct{}, "{\"nil_slice\":[]}"},
{nilMap, "{}"}, {nilMap, "{}"},
{map[string]interface{}{}, "{}"}, {map[string]any{}, "{}"},
{make(map[string]interface{}, 0), "{}"}, {make(map[string]any, 0), "{}"},
{map[string]interface{}{"1": 1, "2": 2, "3": 3}, "{\"1\":1,\"2\":2,\"3\":3}"}, {map[string]any{"1": 1, "2": 2, "3": 3}, "{\"1\":1,\"2\":2,\"3\":3}"},
{pNilMap, "null"}, {pNilMap, "null"},
{nilMapStruct{}, "{\"nil_map\":{}}"}, {nilMapStruct{}, "{\"nil_map\":{}}"},
{testWithFilter{}, "{\"test1\":\"\"}"}, {testWithFilter{}, "{\"test1\":\"\"}"},
+4 -4
View File
@@ -28,10 +28,10 @@ func FuzzUnmarshalJSON(f *testing.F) {
}`)) }`))
f.Fuzz(func(t *testing.T, b []byte) { f.Fuzz(func(t *testing.T, b []byte) {
for _, typ := range []func() interface{}{ for _, typ := range []func() any{
func() interface{} { return new(interface{}) }, func() any { return new(any) },
func() interface{} { return new(map[string]interface{}) }, func() any { return new(map[string]any) },
func() interface{} { return new([]interface{}) }, func() any { return new([]any) },
} { } {
i := typ() i := typ()
if err := Unmarshal(b, i); err != nil { if err := Unmarshal(b, i); err != nil {
+3 -3
View File
@@ -27,11 +27,11 @@ func (r GoJsonRender) Render(w http.ResponseWriter) error {
} }
jsonBytes, err := MarshalSafeCollections(r.Data, r.NilSafeSlices, r.NilSafeMaps, r.Indent, r.Filter) jsonBytes, err := MarshalSafeCollections(r.Data, r.NilSafeSlices, r.NilSafeMaps, r.Indent, r.Filter)
if err != nil { if err != nil {
panic(err) return err
} }
_, err = w.Write(jsonBytes) _, err = w.Write(jsonBytes)
if err != nil { if err != nil {
panic(err) return err
} }
return nil return nil
} }
@@ -39,7 +39,7 @@ func (r GoJsonRender) Render(w http.ResponseWriter) error {
func (r GoJsonRender) RenderString() (string, error) { func (r GoJsonRender) RenderString() (string, error) {
jsonBytes, err := MarshalSafeCollections(r.Data, r.NilSafeSlices, r.NilSafeMaps, r.Indent, r.Filter) jsonBytes, err := MarshalSafeCollections(r.Data, r.NilSafeSlices, r.NilSafeMaps, r.Indent, r.Filter)
if err != nil { if err != nil {
panic(err) return "", err
} }
return string(jsonBytes), nil return string(jsonBytes), nil
} }
+1 -1
View File
@@ -90,7 +90,7 @@ func appendCompact(dst, src []byte, escape bool) ([]byte, error) {
func appendNewline(dst []byte, prefix, indent string, depth int) []byte { func appendNewline(dst []byte, prefix, indent string, depth int) []byte {
dst = append(dst, '\n') dst = append(dst, '\n')
dst = append(dst, prefix...) dst = append(dst, prefix...)
for i := 0; i < depth; i++ { for range depth {
dst = append(dst, indent...) dst = append(dst, indent...)
} }
return dst return dst
+3 -12
View File
@@ -210,10 +210,7 @@ func diff(t *testing.T, a, b []byte) {
t.Helper() t.Helper()
for i := 0; ; i++ { for i := 0; ; i++ {
if i >= len(a) || i >= len(b) || a[i] != b[i] { if i >= len(a) || i >= len(b) || a[i] != b[i] {
j := i - 10 j := max(i-10, 0)
if j < 0 {
j = 0
}
t.Errorf("diverge at %d: «%s» vs «%s»", i, trim(a[j:]), trim(b[j:])) t.Errorf("diverge at %d: «%s» vs «%s»", i, trim(a[j:]), trim(b[j:]))
return return
} }
@@ -274,10 +271,7 @@ func genString(stddev float64) string {
} }
func genArray(n int) []any { func genArray(n int) []any {
f := int(math.Abs(rand.NormFloat64()) * math.Min(10, float64(n/2))) f := min(int(math.Abs(rand.NormFloat64())*math.Min(10, float64(n/2))), n)
if f > n {
f = n
}
if f < 1 { if f < 1 {
f = 1 f = 1
} }
@@ -289,10 +283,7 @@ func genArray(n int) []any {
} }
func genMap(n int) map[string]any { func genMap(n int) map[string]any {
f := int(math.Abs(rand.NormFloat64()) * math.Min(10, float64(n/2))) f := min(int(math.Abs(rand.NormFloat64())*math.Min(10, float64(n/2))), n)
if f > n {
f = n
}
if n > 0 && f == 0 { if n > 0 && f == 0 {
f = 1 f = 1
} }
+1 -2
View File
@@ -1,4 +1,3 @@
Google OAuth Setup (to send mails) Google OAuth Setup (to send mails)
================================== ==================================
@@ -15,7 +14,7 @@ Google OAuth Setup (to send mails)
Kontakt-Email: ... Kontakt-Email: ...
- Unter "Anmeldedaten" neuer OAuth Client erstellen @ https://console.cloud.google.com/apis/credentials - Unter "API und Dienste" > "Anmeldedaten" neuer OAuth Client erstellen @ https://console.cloud.google.com/apis/credentials
Anwendungstyp: Web Anwendungstyp: Web
Name: 'BackendMailOAuth' Name: 'BackendMailOAuth'
Redirect-Uri: 'http://localhost/oauth' Redirect-Uri: 'http://localhost/oauth'
+1 -1
View File
@@ -1,7 +1,7 @@
package googleapi package googleapi
import ( import (
"gogs.mikescher.com/BlackForestBytes/goext/langext" "git.blackforestbytes.com/BlackForestBytes/goext/langext"
"mime" "mime"
"strings" "strings"
"time" "time"
+1 -1
View File
@@ -1,7 +1,7 @@
package googleapi package googleapi
import ( import (
"gogs.mikescher.com/BlackForestBytes/goext/tst" "git.blackforestbytes.com/BlackForestBytes/goext/tst"
"os" "os"
"testing" "testing"
) )
+4 -5
View File
@@ -3,9 +3,8 @@ package googleapi
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"gogs.mikescher.com/BlackForestBytes/goext/exerr" "git.blackforestbytes.com/BlackForestBytes/goext/exerr"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "git.blackforestbytes.com/BlackForestBytes/goext/timeext"
"gogs.mikescher.com/BlackForestBytes/goext/timeext"
"io" "io"
"net/http" "net/http"
"sync" "sync"
@@ -83,8 +82,8 @@ func (c *oauth) AccessToken() (string, error) {
} }
c.lock.Lock() c.lock.Lock()
c.expiryDate = langext.Ptr(reqStartTime.Add(timeext.FromSeconds(r.ExpiresIn - 10))) c.expiryDate = new(reqStartTime.Add(timeext.FromSeconds(r.ExpiresIn - 10)))
c.accessToken = langext.Ptr(r.AccessToken) c.accessToken = new(r.AccessToken)
c.lock.Unlock() c.lock.Unlock()
return r.AccessToken, nil return r.AccessToken, nil
+3 -3
View File
@@ -6,9 +6,9 @@ import (
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"fmt" "fmt"
"gogs.mikescher.com/BlackForestBytes/goext" "git.blackforestbytes.com/BlackForestBytes/goext"
"gogs.mikescher.com/BlackForestBytes/goext/exerr" "git.blackforestbytes.com/BlackForestBytes/goext/exerr"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "git.blackforestbytes.com/BlackForestBytes/goext/langext"
"io" "io"
"net/http" "net/http"
) )
+3 -3
View File
@@ -3,9 +3,9 @@ package googleapi
import ( import (
"context" "context"
"fmt" "fmt"
"gogs.mikescher.com/BlackForestBytes/goext/exerr" "git.blackforestbytes.com/BlackForestBytes/goext/exerr"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "git.blackforestbytes.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/tst" "git.blackforestbytes.com/BlackForestBytes/goext/tst"
"os" "os"
"testing" "testing"
) )
+2 -2
View File
@@ -2,8 +2,8 @@
package imageext package imageext
import "gogs.mikescher.com/BlackForestBytes/goext/langext" import "git.blackforestbytes.com/BlackForestBytes/goext/langext"
import "gogs.mikescher.com/BlackForestBytes/goext/enums" import "git.blackforestbytes.com/BlackForestBytes/goext/enums"
const ChecksumEnumGenerator = "1da5383c33ee442fd0b899369053f66bdc85bed2dbf906949d3edfeedfe13340" // GoExtVersion: 0.0.449 const ChecksumEnumGenerator = "1da5383c33ee442fd0b899369053f66bdc85bed2dbf906949d3edfeedfe13340" // GoExtVersion: 0.0.449
+2 -2
View File
@@ -4,8 +4,8 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"github.com/disintegration/imaging" "github.com/disintegration/imaging"
"gogs.mikescher.com/BlackForestBytes/goext/exerr" "git.blackforestbytes.com/BlackForestBytes/goext/exerr"
"gogs.mikescher.com/BlackForestBytes/goext/mathext" "git.blackforestbytes.com/BlackForestBytes/goext/mathext"
"image" "image"
"image/color" "image/color"
"image/draw" "image/draw"
+61 -41
View File
@@ -3,7 +3,10 @@ package langext
import ( import (
"errors" "errors"
"fmt" "fmt"
"math/rand"
"reflect" "reflect"
"slices"
"strings"
) )
func BoolCount(arr ...bool) int { func BoolCount(arr ...bool) int {
@@ -40,22 +43,12 @@ func ReverseArray[T any](v []T) {
} }
func InArray[T comparable](needle T, haystack []T) bool { func InArray[T comparable](needle T, haystack []T) bool {
for _, v := range haystack { return slices.Contains(haystack, needle)
if v == needle {
return true
}
}
return false
} }
// ArrContains checks if the value is contained in the array (same as InArray, but odther name for better findability) // ArrContains checks if the value is contained in the array (same as InArray, but odther name for better findability)
func ArrContains[T comparable](haystack []T, needle T) bool { func ArrContains[T comparable](haystack []T, needle T) bool {
for _, v := range haystack { return slices.Contains(haystack, needle)
if v == needle {
return true
}
}
return false
} }
func ArrUnique[T comparable](array []T) []T { func ArrUnique[T comparable](array []T) []T {
@@ -118,12 +111,7 @@ func ArrAllErr[T any](arr []T, fn func(T) (bool, error)) (bool, error) {
} }
func ArrNone[T any](arr []T, fn func(T) bool) bool { func ArrNone[T any](arr []T, fn func(T) bool) bool {
for _, av := range arr { return !slices.ContainsFunc(arr, fn)
if fn(av) {
return false
}
}
return true
} }
func ArrNoneErr[T any](arr []T, fn func(T) (bool, error)) (bool, error) { func ArrNoneErr[T any](arr []T, fn func(T) (bool, error)) (bool, error) {
@@ -140,12 +128,7 @@ func ArrNoneErr[T any](arr []T, fn func(T) (bool, error)) (bool, error) {
} }
func ArrAny[T any](arr []T, fn func(T) bool) bool { func ArrAny[T any](arr []T, fn func(T) bool) bool {
for _, av := range arr { return slices.ContainsFunc(arr, fn)
if fn(av) {
return true
}
}
return false
} }
func ArrAnyErr[T any](arr []T, fn func(T) (bool, error)) (bool, error) { func ArrAnyErr[T any](arr []T, fn func(T) (bool, error)) (bool, error) {
@@ -245,7 +228,7 @@ func ArrFirst[T any](arr []T, comp func(v T) bool) (T, bool) {
func ArrFirstOrNil[T any](arr []T, comp func(v T) bool) *T { func ArrFirstOrNil[T any](arr []T, comp func(v T) bool) *T {
for _, v := range arr { for _, v := range arr {
if comp(v) { if comp(v) {
return Ptr(v) return new(v)
} }
} }
return nil return nil
@@ -273,7 +256,7 @@ func ArrLastOrNil[T any](arr []T, comp func(v T) bool) *T {
} }
} }
if found { if found {
return Ptr(result) return new(result)
} else { } else {
return nil return nil
} }
@@ -318,10 +301,8 @@ func ArrLastIndexFunc[T any](arr []T, comp func(v T) bool) int {
} }
func AddToSet[T comparable](set []T, add T) []T { func AddToSet[T comparable](set []T, add T) []T {
for _, v := range set { if slices.Contains(set, add) {
if v == add { return set
return set
}
} }
return append(set, add) return append(set, add)
} }
@@ -384,6 +365,7 @@ func ArrMapErr[T1 any, T2 any](arr []T1, conv func(v T1) (T2, error)) ([]T2, err
return r, nil return r, nil
} }
// ArrFilterMap returns a new array containing only the elements matching the filter, converted via conv (combines ArrFilter and ArrMap)
func ArrFilterMap[T1 any, T2 any](arr []T1, filter func(v T1) bool, conv func(v T1) T2) []T2 { func ArrFilterMap[T1 any, T2 any](arr []T1, filter func(v T1) bool, conv func(v T1) T2) []T2 {
r := make([]T2, 0, len(arr)) r := make([]T2, 0, len(arr))
for _, v := range arr { for _, v := range arr {
@@ -394,6 +376,7 @@ func ArrFilterMap[T1 any, T2 any](arr []T1, filter func(v T1) bool, conv func(v
return r return r
} }
// ArrFilter returns a new array containing only the elements matching the filter
func ArrFilter[T any](arr []T, filter func(v T) bool) []T { func ArrFilter[T any](arr []T, filter func(v T) bool) []T {
r := make([]T, 0, len(arr)) r := make([]T, 0, len(arr))
for _, v := range arr { for _, v := range arr {
@@ -404,6 +387,17 @@ func ArrFilter[T any](arr []T, filter func(v T) bool) []T {
return r return r
} }
// ArrCount returns the number of elements matching the filter
func ArrCount[T any](arr []T, filter func(v T) bool) int {
c := 0
for _, v := range arr {
if filter(v) {
c++
}
}
return c
}
func ArrSum[T NumberConstraint](arr []T) T { func ArrSum[T NumberConstraint](arr []T) T {
var r T = 0 var r T = 0
for _, v := range arr { for _, v := range arr {
@@ -508,7 +502,7 @@ func ArrAppend[T any](arr []T, add ...T) []T {
func ArrPrepend[T any](arr []T, add ...T) []T { func ArrPrepend[T any](arr []T, add ...T) []T {
out := make([]T, len(arr)+len(add)) out := make([]T, len(arr)+len(add))
copy(out[len(add):], arr) copy(out[len(add):], arr)
for i := 0; i < len(add); i++ { for i := range add {
out[len(add)-i-1] = add[i] out[len(add)-i-1] = add[i]
} }
return out return out
@@ -544,8 +538,8 @@ func ArrExcept[T comparable](arr []T, needles ...T) []T {
return r return r
} }
func ArrayToInterface[T any](t []T) []interface{} { func ArrayToInterface[T any](t []T) []any {
res := make([]interface{}, 0, len(t)) res := make([]any, 0, len(t))
for i, _ := range t { for i, _ := range t {
res = append(res, t[i]) res = append(res, t[i])
} }
@@ -553,15 +547,15 @@ func ArrayToInterface[T any](t []T) []interface{} {
} }
func JoinString(arr []string, delimiter string) string { func JoinString(arr []string, delimiter string) string {
str := "" var str strings.Builder
for i, v := range arr { for i, v := range arr {
str += v str.WriteString(v)
if i < len(arr)-1 { if i < len(arr)-1 {
str += delimiter str.WriteString(delimiter)
} }
} }
return str return str.String()
} }
// ArrChunk splits the array into buckets of max-size `chunkSize` // ArrChunk splits the array into buckets of max-size `chunkSize`
@@ -581,10 +575,7 @@ func ArrChunk[T any](arr []T, chunkSize int) [][]T {
i := 0 i := 0
for i < len(arr) { for i < len(arr) {
right := i + chunkSize right := min(i+chunkSize, len(arr))
if right >= len(arr) {
right = len(arr)
}
res = append(res, arr[i:right]) res = append(res, arr[i:right])
@@ -608,3 +599,32 @@ func ArrGroupBy[T1 any, T2 comparable](arr []T1, groupfunc func(v T1) T2) map[T2
return r return r
} }
func ArrShuffle[T any](arr []T) []T {
for i := range arr {
j := rand.Intn(i + 1)
arr[i], arr[j] = arr[j], arr[i]
}
return arr
}
func ArrMax[T NumberConstraint](first T, arr []T) T {
res := first
for _, v := range arr {
if v > res {
res = v
}
}
return res
}
func ArrMin[T NumberConstraint](first T, arr []T) T {
res := first
for _, v := range arr {
if v < res {
res = v
}
}
return res
}
+1 -1
View File
@@ -1,7 +1,7 @@
package langext package langext
import ( import (
"gogs.mikescher.com/BlackForestBytes/goext/tst" "git.blackforestbytes.com/BlackForestBytes/goext/tst"
"strings" "strings"
"testing" "testing"
) )
+4 -7
View File
@@ -44,8 +44,8 @@ func newBase58Encoding(alphabet string) *B58Encoding {
b58 := make([]byte, 0, 256) b58 := make([]byte, 0, 256)
for i := byte(0); i < 32; i++ { for i := range byte(32) {
for j := byte(0); j < 8; j++ { for j := range byte(8) {
b := i*8 + j b := i*8 + j
@@ -100,7 +100,7 @@ func (enc *B58Encoding) Encode(src []byte) ([]byte, error) {
} }
} else { } else {
m := mod.Int64() m := mod.Int64()
for i := 0; i < 10; i++ { for range 10 {
answer = append(answer, enc.alphabet[m%58]) answer = append(answer, enc.alphabet[m%58])
m /= 58 m /= 58
} }
@@ -137,10 +137,7 @@ func (enc *B58Encoding) Decode(src []byte) ([]byte, error) {
scratch := new(big.Int) scratch := new(big.Int)
for t := src; len(t) > 0; { for t := src; len(t) > 0; {
n := len(t) n := min(len(t), 10)
if n > 10 {
n = 10
}
total := uint64(0) total := uint64(0)
for _, v := range t[:n] { for _, v := range t[:n] {
+1 -1
View File
@@ -1,7 +1,7 @@
package langext package langext
import ( import (
"gogs.mikescher.com/BlackForestBytes/goext/tst" "git.blackforestbytes.com/BlackForestBytes/goext/tst"
"testing" "testing"
) )
+4 -4
View File
@@ -18,18 +18,18 @@ func RandBase62(rlen int) string {
randMax := big.NewInt(math.MaxInt64) randMax := big.NewInt(math.MaxInt64)
r := "" var r strings.Builder
for i := 0; i < rlen; i++ { for range rlen {
v, err := rand.Int(rand.Reader, randMax) v, err := rand.Int(rand.Reader, randMax)
if err != nil { if err != nil {
panic(err) panic(err)
} }
r += string(base62CharacterSet[v.Mod(v, bi52).Int64()]) r.WriteString(string(base62CharacterSet[v.Mod(v, bi52).Int64()]))
} }
return r return r.String()
} }
func EncodeBase62(num uint64) string { func EncodeBase62(num uint64) string {
+23
View File
@@ -0,0 +1,23 @@
package langext
import (
"encoding/base64"
"strings"
)
// DecodeBase64Any decodes a base64 encoded string
// Works with all variants (std, url, imap), padded and unpadded and even ignores linrebreaks and indents
func DecodeBase64Any(data string) ([]byte, error) {
data = strings.ReplaceAll(data, "\n", "") // remove linebreaks and indents
data = strings.ReplaceAll(data, "\t", "") // remove linebreaks and indents
data = strings.ReplaceAll(data, " ", "") // remove linebreaks and indents
data = strings.ReplaceAll(data, ",", "/") // base64_imap --> base64_std
data = strings.ReplaceAll(data, "_", "/") // base64_url --> base64_std
data = strings.ReplaceAll(data, "-", "+") // base64_url --> base64_std
data = strings.ReplaceAll(data, "=", "") // no padding
return base64.RawStdEncoding.DecodeString(data)
}
+246
View File
@@ -0,0 +1,246 @@
package langext
import (
"testing"
)
func TestDecodeBase64Any_StandardPadded(t *testing.T) {
input := "SGVsbG8gV29ybGQ=" // "Hello World" in standard Base64 (padded)
expected := "Hello World"
result, err := DecodeBase64Any(input)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if string(result) != expected {
t.Errorf("expected %q, got %q", expected, string(result))
}
}
func TestDecodeBase64Any_StandardUnpadded(t *testing.T) {
input := "SGVsbG8gV29ybGQ" // "Hello World" in standard Base64 (unpadded)
expected := "Hello World"
result, err := DecodeBase64Any(input)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if string(result) != expected {
t.Errorf("expected %q, got %q", expected, string(result))
}
}
func TestDecodeBase64Any_URLPadded(t *testing.T) {
input := "SGVsbG8tV29ybGQ=" // "Hello-World" in Base64 URL (padded)
expected := "Hello-World"
result, err := DecodeBase64Any(input)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if string(result) != expected {
t.Errorf("expected %q, got %q", expected, string(result))
}
}
func TestDecodeBase64Any_URLUnpadded(t *testing.T) {
input := "SGVsbG8tV29ybGQ" // "Hello-World" in Base64 URL (unpadded)
expected := "Hello-World"
result, err := DecodeBase64Any(input)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if string(result) != expected {
t.Errorf("expected %q, got %q", expected, string(result))
}
}
func TestDecodeBase64Any_IMAPPadded(t *testing.T) {
input := "SGVsbG8,V29ybGQ=" // "Hello/World" in Base64 IMAP (padded)
expected := "Hello?World"
result, err := DecodeBase64Any(input)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if string(result) != expected {
t.Errorf("expected %q, got %q", expected, string(result))
}
}
func TestDecodeBase64Any_IMAPUnpadded(t *testing.T) {
input := "SGVsbG8,V29ybGQ" // "Hello/World" in Base64 IMAP (unpadded)
expected := "Hello?World"
result, err := DecodeBase64Any(input)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if string(result) != expected {
t.Errorf("expected %q, got %q", expected, string(result))
}
}
func TestDecodeBase64Any_InvalidInput(t *testing.T) {
input := "Invalid@@Base64" // Invalid Base64 input
_, err := DecodeBase64Any(input)
if err == nil {
t.Fatal("expected an error, but got none")
}
}
func TestDecodeBase64Any_EmptyInput(t *testing.T) {
input := "" // Empty input
result, err := DecodeBase64Any(input)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(result) != 0 {
t.Errorf("expected empty result, got %q", string(result))
}
}
func TestDecodeBase64Any_WhitespaceInput(t *testing.T) {
input := " SGVsbG8gV29ybGQ= " // Input with leading and trailing spaces
expected := "Hello World"
result, err := DecodeBase64Any(input)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if string(result) != expected {
t.Errorf("expected %q, got %q", expected, string(result))
}
}
func TestDecodeBase64Any_LineBreaksInput(t *testing.T) {
input := "SGVs\nbG8g\nV29y\nbGQ=" // Input with line breaks
expected := "Hello World"
result, err := DecodeBase64Any(input)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if string(result) != expected {
t.Errorf("expected %q, got %q", expected, string(result))
}
}
func TestDecodeBase64Any_TabCharactersInput(t *testing.T) {
input := "SGVsbG8g\tV29ybGQ=" // Input with tab characters
expected := "Hello World"
result, err := DecodeBase64Any(input)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if string(result) != expected {
t.Errorf("expected %q, got %q", expected, string(result))
}
}
func TestDecodeBase64Any_SpecialCharactersIgnored(t *testing.T) {
input := "SGVsbG8gV29ybGQ=!!" // Input with ignored special characters
_, err := DecodeBase64Any(input)
if err == nil {
t.Fatal("expected an error, but got none")
}
}
func TestDecodeBase64Any_SingleCharacterInput(t *testing.T) {
input := "QQ==" // "A" in Base64
expected := "A"
result, err := DecodeBase64Any(input)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if string(result) != expected {
t.Errorf("expected %q, got %q", expected, string(result))
}
}
func TestDecodeBase64Any_LongInput(t *testing.T) {
input := "U29tZSB2ZXJ5IGxvbmcgc3RyaW5nIHdpdGggbXVsdGlwbGUgbGluZXMgYW5kIHNwYWNlcy4=" // Long Base64 string
expected := "Some very long string with multiple lines and spaces."
result, err := DecodeBase64Any(input)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if string(result) != expected {
t.Errorf("expected %q, got %q", expected, string(result))
}
}
func TestDecodeBase64Any_Standard63And64(t *testing.T) {
input := "Pz8/Pw==" // "???" in standard Base64 (63 = '+', 64 = '/')
expected := "????"
result, err := DecodeBase64Any(input)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if string(result) != expected {
t.Errorf("expected %q, got %q", expected, string(result))
}
}
func TestDecodeBase64Any_Standard63And64_NoPad(t *testing.T) {
input := "Pz8/Pw" // "???" in standard Base64 (63 = '+', 64 = '/')
expected := "????"
result, err := DecodeBase64Any(input)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if string(result) != expected {
t.Errorf("expected %q, got %q", expected, string(result))
}
}
func TestDecodeBase64Any_URL63And64(t *testing.T) {
input := "Pz8_Pw==" // "???" in Base64 URL-safe (63 = '_', 64 = '-')
expected := "????"
result, err := DecodeBase64Any(input)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if string(result) != expected {
t.Errorf("expected %q, got %q", expected, string(result))
}
}
func TestDecodeBase64Any_URL63And64_NoPad(t *testing.T) {
input := "Pz8_Pw==" // "???" in Base64 URL-safe (63 = '_', 64 = '-')
expected := "????"
result, err := DecodeBase64Any(input)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if string(result) != expected {
t.Errorf("expected %q, got %q", expected, string(result))
}
}
+8 -7
View File
@@ -5,6 +5,7 @@ import (
"errors" "errors"
"math" "math"
"math/big" "math/big"
"strings"
) )
type AnyBaseConverter struct { type AnyBaseConverter struct {
@@ -25,18 +26,18 @@ func (bc AnyBaseConverter) Rand(rlen int) string {
randMax := big.NewInt(math.MaxInt64) randMax := big.NewInt(math.MaxInt64)
r := "" var r strings.Builder
for i := 0; i < rlen; i++ { for range rlen {
v, err := rand.Int(rand.Reader, randMax) v, err := rand.Int(rand.Reader, randMax)
if err != nil { if err != nil {
panic(err) panic(err)
} }
r += string(bc.charset[v.Mod(v, biBase).Int64()]) r.WriteString(string(bc.charset[v.Mod(v, biBase).Int64()]))
} }
return r return r.String()
} }
func (bc AnyBaseConverter) EncodeUInt64(num uint64) string { func (bc AnyBaseConverter) EncodeUInt64(num uint64) string {
@@ -44,7 +45,7 @@ func (bc AnyBaseConverter) EncodeUInt64(num uint64) string {
return "0" return "0"
} }
b := "" var b strings.Builder
// loop as long the num is bigger than zero // loop as long the num is bigger than zero
for num > 0 { for num > 0 {
@@ -53,10 +54,10 @@ func (bc AnyBaseConverter) EncodeUInt64(num uint64) string {
num -= r num -= r
num /= base62Base num /= base62Base
b += string(bc.charset[int(r)]) b.WriteString(string(bc.charset[int(r)]))
} }
return b return b.String()
} }
func (bc AnyBaseConverter) DecodeUInt64(str string) (uint64, error) { func (bc AnyBaseConverter) DecodeUInt64(str string) (uint64, error) {
+1 -1
View File
@@ -1,7 +1,7 @@
package langext package langext
import ( import (
"gogs.mikescher.com/BlackForestBytes/goext/tst" "git.blackforestbytes.com/BlackForestBytes/goext/tst"
"testing" "testing"
) )
+1 -1
View File
@@ -25,7 +25,7 @@ func BytesXOR(a []byte, b []byte) ([]byte, error) {
r := make([]byte, len(a)) r := make([]byte, len(a))
for i := 0; i < len(a); i++ { for i := range a {
r[i] = a[i] ^ b[i] r[i] = a[i] ^ b[i]
} }

Some files were not shown because too many files have changed in this diff Show More