Compare commits

...

67 Commits

Author SHA1 Message Date
0c05bcf29b v0.0.221 download file data 2023-08-06 19:10:31 +02:00
9136143f2f v0.0.220 add ginext.bufferBody 2023-08-03 09:09:27 +02:00
2f1b784dc2 v0.0.219 implement error.Is(*) for exerr 2023-07-28 15:42:12 +02:00
190584e0e6 v0.0.218 bf 2023-07-27 17:16:30 +02:00
b7003b9ec9 v0.0.217 2023-07-27 17:12:41 +02:00
4f871271e8 v0.0.216 2023-07-27 17:00:53 +02:00
91f4793678 v0.0.215 Add (ee *ExErr) ToAPIJson 2023-07-27 14:37:11 +02:00
3b30bb049e v0.0.214 reassign innerctx 2023-07-27 09:58:10 +02:00
f0c5b36ea9 v0.0.213 inject gin key value pairs into context 2023-07-27 09:46:06 +02:00
647ec64c3b v0.0.212 2023-07-26 10:44:26 +02:00
b5f9b6b638 v0.0.211 2023-07-26 10:40:42 +02:00
c7949febf2 v0.0.210 fix ginext route dump 2023-07-25 11:16:11 +02:00
15a4b2a713 v0.0.209 removed g context from err func 2023-07-25 10:56:03 +02:00
493c6ebae8 v0.0.208 remove context from err functions because its not used 2023-07-25 10:51:14 +02:00
fb847b03af v0.0.207 renamed APIError to Error 2023-07-25 10:47:00 +02:00
f826633e6e v0.0.206 2023-07-24 18:50:14 +02:00
edeae23bf1 v0.0.205 2023-07-24 18:47:48 +02:00
a038b86147 v0.0.204 2023-07-24 18:42:33 +02:00
ede0b99d3a v0.0.203 2023-07-24 18:38:04 +02:00
d04ce18eb0 v0.0.202 2023-07-24 18:34:56 +02:00
8ae9a0f107 v0.0.201 2023-07-24 18:22:36 +02:00
a259bb6dbc v0.0.200 2023-07-24 17:42:18 +02:00
adf32568ee v0.0.199 2023-07-24 17:23:38 +02:00
0cfa159cb1 v0.0.198 2023-07-24 14:16:02 +02:00
0ead99608a v0.0.197 2023-07-24 12:27:06 +02:00
7fe3e66cad v0.0.196 2023-07-24 11:47:47 +02:00
a73d7d1654 v0.0.195 2023-07-24 11:42:52 +02:00
bbd7a7bc2c v0.0.194 2023-07-24 11:40:47 +02:00
f5151eb214 v0.0.193 2023-07-24 11:38:57 +02:00
eefb9ac9f5 v0.0.192 2023-07-24 11:30:07 +02:00
468a7d212d v0.0.191 2023-07-24 11:18:25 +02:00
a4def75d06 v0.0.190 2023-07-24 11:16:57 +02:00
16c66ee28c v0.0.189 2023-07-24 11:11:15 +02:00
2e6ca48d22 v0.0.188 exerr MVP 2023-07-24 10:42:39 +02:00
b1d6509294 v0.0.187 forget to use function 2023-07-24 09:16:37 +02:00
e909d656d9 v0.0.186 convert array to interface arr 2023-07-24 09:13:19 +02:00
0971f60c30 v0.0.185 add Meta() to enums 2023-07-19 19:34:39 +02:00
d8270e53ed v0.0.184 re-add missing array methods from merge commit 56684b2c0b 2023-07-19 19:29:59 +02:00
1ee127937a v0.0.183 2023-07-19 19:24:58 +02:00
56684b2c0b exerr [WIP]
(cherry picked from commit c0443af63b)
2023-07-19 19:24:43 +02:00
1ea6695f82 v0.0.182 added optional commit message 2023-07-19 11:26:23 +02:00
5273ff7600 v0.0.181 2023-07-19 11:26:06 +02:00
caa69c3629 v0.0.180 Super Test 2023-07-19 11:24:11 +02:00
0ff5f0aa28 v0.0.179 TestTest 2023-07-19 11:22:25 +02:00
d5cb1e48ed v0.0.178 2023-07-19 11:20:35 +02:00
1c2d3f541f v0.0.177 2023-07-18 16:08:24 +02:00
ec62ad436f v0.0.176 2023-07-18 16:01:34 +02:00
8d0ef0f002 v0.0.175 2023-07-18 15:59:12 +02:00
d78550672e v0.0.174 2023-07-18 15:23:32 +02:00
1d629f6db8 v0.0.173 2023-07-18 15:12:06 +02:00
f7d291056d v0.0.172 2023-07-18 14:40:10 +02:00
710c257c64 v0.0.171 2023-07-18 13:34:54 +02:00
c320bb3d90 v0.0.170 2023-07-17 12:42:49 +02:00
2f01a1d50f v0.0.169 2023-07-05 19:27:49 +02:00
ffc57b7e89 v0.0.168 2023-07-05 19:27:15 +02:00
d88cd3c22b v0.0.167 2023-06-22 17:33:56 +02:00
ac5ad640bd v0.0.166 2023-06-22 15:07:06 +02:00
21d241f9b1 v0.0.163 2023-06-18 01:16:52 +02:00
2569c165f8 v0.0.162 2023-06-11 16:38:47 +02:00
ee262a94fb v0.0.161 2023-06-11 16:35:20 +02:00
7977c0e59c Added rfctime.Date type 2023-06-10 19:13:15 +02:00
ceff0161c6 v0.0.159 2023-06-10 18:35:56 +02:00
a30da61419 v0.0.158 2023-06-10 16:28:50 +02:00
b613b122e3 v0.0.157 2023-06-10 16:22:14 +02:00
d017530444 v0.0.156 2023-06-10 00:19:17 +02:00
8de83cc290 v0.0.155 2023-06-08 16:26:06 +02:00
603ec82b83 v0.0.154 2023-06-08 16:24:53 +02:00
47 changed files with 4051 additions and 191 deletions

View File

@@ -21,6 +21,11 @@ if [ "$( git rev-parse --abbrev-ref HEAD )" != "master" ]; then
exit 1 exit 1
fi fi
echo ""
echo -n "Insert optional commit message: "
read commitMessage
echo ""
git pull --ff git pull --ff
go get -u ./... go get -u ./...
@@ -40,6 +45,11 @@ git add --verbose .
msg="v${next_ver}" msg="v${next_ver}"
if [[ "$commitMessage" != "" ]]; then
msg="${msg} ${commitMessage}"
fi
if [ $# -gt 0 ]; then if [ $# -gt 0 ]; then
msg="$1" msg="$1"
fi fi

View File

@@ -11,6 +11,7 @@ import (
"io" "io"
"os" "os"
"path" "path"
"path/filepath"
"regexp" "regexp"
"strings" "strings"
"time" "time"
@@ -24,18 +25,19 @@ type EnumDefVal struct {
type EnumDef struct { type EnumDef struct {
File string File string
FileRelative string
EnumTypeName string EnumTypeName string
Type string Type string
Values []EnumDefVal Values []EnumDefVal
} }
var rexPackage = rext.W(regexp.MustCompile("^package\\s+(?P<name>[A-Za-z0-9_]+)\\s*$")) var rexPackage = rext.W(regexp.MustCompile(`^package\s+(?P<name>[A-Za-z0-9_]+)\s*$`))
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 rexValueDef = rext.W(regexp.MustCompile("^\\s*(?P<name>[A-Za-z0-9_]+)\\s+(?P<type>[A-Za-z0-9_]+)\\s*=\\s*(?P<value>(\"[A-Za-z0-9_:]+\"|[0-9]+))\\s*(//(?P<descr>.*))?.*$")) var rexValueDef = rext.W(regexp.MustCompile(`^\s*(?P<name>[A-Za-z0-9_]+)\s+(?P<type>[A-Za-z0-9_]+)\s*=\s*(?P<value>("[A-Za-z0-9_:]+"|[0-9]+))\s*(//(?P<descr>.*))?.*$`))
var rexChecksumConst = rext.W(regexp.MustCompile("const ChecksumGenerator = \"(?P<cs>[A-Za-z0-9_]*)\"")) var rexChecksumConst = rext.W(regexp.MustCompile(`const ChecksumGenerator = "(?P<cs>[A-Za-z0-9_]*)"`))
func GenerateEnumSpecs(sourceDir string, destFile string) error { func GenerateEnumSpecs(sourceDir string, destFile string) error {
@@ -83,7 +85,7 @@ func GenerateEnumSpecs(sourceDir string, destFile string) error {
for _, f := range files { for _, f := range files {
fmt.Printf("========= %s =========\n\n", f.Name()) fmt.Printf("========= %s =========\n\n", f.Name())
fileEnums, pn, err := processFile(path.Join(sourceDir, f.Name())) fileEnums, pn, err := processFile(sourceDir, path.Join(sourceDir, f.Name()))
if err != nil { if err != nil {
return err return err
} }
@@ -123,7 +125,7 @@ func GenerateEnumSpecs(sourceDir string, destFile string) error {
return nil return nil
} }
func processFile(fn string) ([]EnumDef, string, error) { func processFile(basedir string, fn string) ([]EnumDef, string, error) {
file, err := os.Open(fn) file, err := os.Open(fn)
if err != nil { if err != nil {
return nil, "", err return nil, "", err
@@ -153,8 +155,15 @@ func processFile(fn string) ([]EnumDef, string, error) {
} }
if match, ok := rexEnumDef.MatchFirst(line); ok { if match, ok := rexEnumDef.MatchFirst(line); ok {
rfp, err := filepath.Rel(basedir, fn)
if err != nil {
return nil, "", err
}
def := EnumDef{ def := EnumDef{
File: fn, File: fn,
FileRelative: rfp,
EnumTypeName: match.GroupByName("name").Value(), EnumTypeName: match.GroupByName("name").Value(),
Type: match.GroupByName("type").Value(), Type: match.GroupByName("type").Value(),
Values: make([]EnumDefVal, 0), Values: make([]EnumDefVal, 0),
@@ -239,7 +248,7 @@ func fmtOutput(cs string, enums []EnumDef, pkgname string) string {
str += "// ================================ " + enumdef.EnumTypeName + " ================================" + "\n" str += "// ================================ " + enumdef.EnumTypeName + " ================================" + "\n"
str += "//" + "\n" str += "//" + "\n"
str += "// File: " + enumdef.File + "\n" str += "// File: " + enumdef.FileRelative + "\n"
str += "// StringEnum: " + langext.Conditional(hasStr, "true", "false") + "\n" str += "// StringEnum: " + langext.Conditional(hasStr, "true", "false") + "\n"
str += "// DescrEnum: " + langext.Conditional(hasDescr, "true", "false") + "\n" str += "// DescrEnum: " + langext.Conditional(hasDescr, "true", "false") + "\n"
str += "//" + "\n" str += "//" + "\n"
@@ -284,15 +293,7 @@ func fmtOutput(cs string, enums []EnumDef, pkgname string) string {
str += "" + "\n" str += "" + "\n"
str += "func (e " + enumdef.EnumTypeName + ") ValuesMeta() []EnumMetaValue {" + "\n" str += "func (e " + enumdef.EnumTypeName + ") ValuesMeta() []EnumMetaValue {" + "\n"
str += " return []EnumMetaValue{" + "\n" str += " return " + enumdef.EnumTypeName + "ValuesMeta()"
for _, v := range enumdef.Values {
if hasDescr {
str += " " + fmt.Sprintf("EnumMetaValue{VarName: \"%s\", Value: %s, Description: langext.Ptr(\"%s\")},", v.VarName, v.VarName, strings.TrimSpace(*v.Description)) + "\n"
} else {
str += " " + fmt.Sprintf("EnumMetaValue{VarName: \"%s\", Value: %s, Description: nil},", v.VarName, v.VarName) + "\n"
}
}
str += " }" + "\n"
str += "}" + "\n" str += "}" + "\n"
str += "" + "\n" str += "" + "\n"
@@ -321,6 +322,15 @@ func fmtOutput(cs string, enums []EnumDef, pkgname string) string {
str += "}" + "\n" str += "}" + "\n"
str += "" + "\n" str += "" + "\n"
str += "func (e " + enumdef.EnumTypeName + ") Meta() EnumMetaValue {" + "\n"
if hasDescr {
str += " return EnumMetaValue{VarName: e.VarName(), Value: e, Description: langext.Ptr(e.Description())}"
} else {
str += " return EnumMetaValue{VarName: e.VarName(), Value: e, Description: nil}"
}
str += "}" + "\n"
str += "" + "\n"
str += "func Parse" + enumdef.EnumTypeName + "(vv string) (" + enumdef.EnumTypeName + ", bool) {" + "\n" str += "func Parse" + enumdef.EnumTypeName + "(vv string) (" + enumdef.EnumTypeName + ", bool) {" + "\n"
str += " for _, ev := range __" + enumdef.EnumTypeName + "Values {" + "\n" str += " for _, ev := range __" + enumdef.EnumTypeName + "Values {" + "\n"
str += " if string(ev) == vv {" + "\n" str += " if string(ev) == vv {" + "\n"
@@ -339,11 +349,7 @@ func fmtOutput(cs string, enums []EnumDef, pkgname string) string {
str += "func " + enumdef.EnumTypeName + "ValuesMeta() []EnumMetaValue {" + "\n" str += "func " + enumdef.EnumTypeName + "ValuesMeta() []EnumMetaValue {" + "\n"
str += " return []EnumMetaValue{" + "\n" str += " return []EnumMetaValue{" + "\n"
for _, v := range enumdef.Values { for _, v := range enumdef.Values {
if hasDescr { str += " " + v.VarName + ".Meta(),\n"
str += " " + fmt.Sprintf("EnumMetaValue{VarName: \"%s\", Value: %s, Description: langext.Ptr(\"%s\")},", v.VarName, v.VarName, strings.TrimSpace(*v.Description)) + "\n"
} else {
str += " " + fmt.Sprintf("EnumMetaValue{VarName: \"%s\", Value: %s, Description: nil},", v.VarName, v.VarName) + "\n"
}
} }
str += " }" + "\n" str += " }" + "\n"
str += "}" + "\n" str += "}" + "\n"

View File

@@ -178,9 +178,9 @@ func parseEnvToValue(envval string, fullEnvKey string, rvtype reflect.Type) (ref
if strings.TrimSpace(strings.ToLower(envval)) == "true" { if strings.TrimSpace(strings.ToLower(envval)) == "true" {
return reflect.ValueOf(true).Convert(rvtype), nil return reflect.ValueOf(true).Convert(rvtype), nil
} else if strings.TrimSpace(strings.ToLower(envval)) == "false" { } else if strings.TrimSpace(strings.ToLower(envval)) == "false" {
return reflect.ValueOf(true).Convert(rvtype), nil
} else if strings.TrimSpace(strings.ToLower(envval)) == "1" {
return reflect.ValueOf(false).Convert(rvtype), nil return reflect.ValueOf(false).Convert(rvtype), nil
} else if strings.TrimSpace(strings.ToLower(envval)) == "1" {
return reflect.ValueOf(true).Convert(rvtype), nil
} else if strings.TrimSpace(strings.ToLower(envval)) == "0" { } else if strings.TrimSpace(strings.ToLower(envval)) == "0" {
return reflect.ValueOf(false).Convert(rvtype), nil return reflect.ValueOf(false).Convert(rvtype), nil
} else { } else {

View File

@@ -3,6 +3,7 @@ package cryptext
import ( import (
"crypto/rand" "crypto/rand"
"crypto/sha256" "crypto/sha256"
"crypto/sha512"
"encoding/base64" "encoding/base64"
"encoding/hex" "encoding/hex"
"errors" "errors"
@@ -14,14 +15,15 @@ import (
"strings" "strings"
) )
const LatestPassHashVersion = 4 const LatestPassHashVersion = 5
// PassHash // PassHash
// - [v0]: plaintext password ( `0|...` ) // - [v0]: plaintext password ( `0|...` ) // simple, used to write PW's directly in DB
// - [v1]: sha256(plaintext) // - [v1]: sha256(plaintext) // simple hashing
// - [v2]: seed | sha256<seed>(plaintext) // - [v2]: seed | sha256<seed>(plaintext) // add seed
// - [v3]: seed | sha256<seed>(plaintext) | [hex(totp)] // - [v3]: seed | sha256<seed>(plaintext) | [hex(totp)] // add TOTP support
// - [v4]: bcrypt(plaintext) | [hex(totp)] // - [v4]: bcrypt(plaintext) | [hex(totp)] // use proper bcrypt
// - [v5]: bcrypt(sha512(plaintext)) | [hex(totp)] // hash pw before bcrypt (otherwise max pw-len = 72)
type PassHash string type PassHash string
func (ph PassHash) Valid() bool { func (ph PassHash) Valid() bool {
@@ -109,7 +111,21 @@ func (ph PassHash) Data() (_version int, _seed []byte, _payload []byte, _totp bo
totp := false totp := false
totpsecret := make([]byte, 0) totpsecret := make([]byte, 0)
if split[2] != "0" { if split[2] != "0" {
totpsecret, err = hex.DecodeString(split[3]) totpsecret, err = hex.DecodeString(split[2])
totp = true
}
return int(version), nil, payload, totp, totpsecret, true
}
if version == 5 {
if len(split) != 3 {
return -1, nil, nil, false, nil, false
}
payload := []byte(split[1])
totp := false
totpsecret := make([]byte, 0)
if split[2] != "0" {
totpsecret, err = hex.DecodeString(split[2])
totp = true totp = true
} }
return int(version), nil, payload, totp, totpsecret, true return int(version), nil, payload, totp, totpsecret, true
@@ -156,6 +172,14 @@ func (ph PassHash) Verify(plainpass string, totp *string) bool {
} }
} }
if version == 5 {
if !hastotp {
return bcrypt.CompareHashAndPassword(payload, hash512(plainpass)) == nil
} else {
return bcrypt.CompareHashAndPassword(payload, hash512(plainpass)) == nil && totpext.Validate(totpsecret, *totp)
}
}
return false return false
} }
@@ -209,6 +233,12 @@ func (ph PassHash) ClearTOTP() (PassHash, error) {
return PassHash(strings.Join(split, "|")), nil return PassHash(strings.Join(split, "|")), nil
} }
if version == 5 {
split := strings.Split(string(ph), "|")
split[2] = "0"
return PassHash(strings.Join(split, "|")), nil
}
return "", errors.New("unknown version") return "", errors.New("unknown version")
} }
@@ -242,6 +272,12 @@ func (ph PassHash) WithTOTP(totpSecret []byte) (PassHash, error) {
return PassHash(strings.Join(split, "|")), nil return PassHash(strings.Join(split, "|")), nil
} }
if version == 5 {
split := strings.Split(string(ph), "|")
split[2] = hex.EncodeToString(totpSecret)
return PassHash(strings.Join(split, "|")), nil
}
return "", errors.New("unknown version") return "", errors.New("unknown version")
} }
@@ -271,6 +307,10 @@ func (ph PassHash) Change(newPlainPass string) (PassHash, error) {
return HashPasswordV4(newPlainPass, langext.Conditional(hastotp, totpsecret, nil)) return HashPasswordV4(newPlainPass, langext.Conditional(hastotp, totpsecret, nil))
} }
if version == 5 {
return HashPasswordV5(newPlainPass, langext.Conditional(hastotp, totpsecret, nil))
}
return "", errors.New("unknown version") return "", errors.New("unknown version")
} }
@@ -279,7 +319,24 @@ func (ph PassHash) String() string {
} }
func HashPassword(plainpass string, totpSecret []byte) (PassHash, error) { func HashPassword(plainpass string, totpSecret []byte) (PassHash, error) {
return HashPasswordV4(plainpass, totpSecret) return HashPasswordV5(plainpass, totpSecret)
}
func HashPasswordV5(plainpass string, totpSecret []byte) (PassHash, error) {
var strtotp string
if totpSecret == nil {
strtotp = "0"
} else {
strtotp = hex.EncodeToString(totpSecret)
}
payload, err := bcrypt.GenerateFromPassword(hash512(plainpass), bcrypt.MinCost)
if err != nil {
return "", err
}
return PassHash(fmt.Sprintf("5|%s|%s", string(payload), strtotp)), nil
} }
func HashPasswordV4(plainpass string, totpSecret []byte) (PassHash, error) { func HashPasswordV4(plainpass string, totpSecret []byte) (PassHash, error) {
@@ -340,6 +397,13 @@ func HashPasswordV0(plainpass string) (PassHash, error) {
return PassHash(fmt.Sprintf("0|%s", plainpass)), nil return PassHash(fmt.Sprintf("0|%s", plainpass)), nil
} }
func hash512(s string) []byte {
h := sha512.New()
h.Write([]byte(s))
bs := h.Sum(nil)
return bs
}
func hash256(s string) []byte { func hash256(s string) []byte {
h := sha256.New() h := sha256.New()
h.Write([]byte(s)) h.Write([]byte(s))

210
cryptext/passHash_test.go Normal file
View File

@@ -0,0 +1,210 @@
package cryptext
import (
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/totpext"
"gogs.mikescher.com/BlackForestBytes/goext/tst"
"testing"
)
func TestPassHash1(t *testing.T) {
ph, err := HashPassword("test123", nil)
tst.AssertNoErr(t, err)
tst.AssertTrue(t, ph.Valid())
tst.AssertFalse(t, ph.HasTOTP())
tst.AssertFalse(t, ph.NeedsPasswordUpgrade())
tst.AssertTrue(t, ph.Verify("test123", nil))
tst.AssertFalse(t, ph.Verify("test124", nil))
}
func TestPassHashTOTP(t *testing.T) {
sec, err := totpext.GenerateSecret()
tst.AssertNoErr(t, err)
ph, err := HashPassword("test123", sec)
tst.AssertNoErr(t, err)
tst.AssertTrue(t, ph.Valid())
tst.AssertTrue(t, ph.HasTOTP())
tst.AssertFalse(t, ph.NeedsPasswordUpgrade())
tst.AssertFalse(t, ph.Verify("test123", nil))
tst.AssertFalse(t, ph.Verify("test124", nil))
tst.AssertTrue(t, ph.Verify("test123", langext.Ptr(totpext.TOTP(sec))))
tst.AssertFalse(t, ph.Verify("test124", nil))
}
func TestPassHashUpgrade_V0(t *testing.T) {
ph, err := HashPasswordV0("test123")
tst.AssertNoErr(t, err)
tst.AssertTrue(t, ph.Valid())
tst.AssertFalse(t, ph.HasTOTP())
tst.AssertTrue(t, ph.NeedsPasswordUpgrade())
tst.AssertTrue(t, ph.Verify("test123", nil))
tst.AssertFalse(t, ph.Verify("test124", nil))
ph, err = ph.Upgrade("test123")
tst.AssertNoErr(t, err)
tst.AssertTrue(t, ph.Valid())
tst.AssertFalse(t, ph.HasTOTP())
tst.AssertFalse(t, ph.NeedsPasswordUpgrade())
tst.AssertTrue(t, ph.Verify("test123", nil))
tst.AssertFalse(t, ph.Verify("test124", nil))
}
func TestPassHashUpgrade_V1(t *testing.T) {
ph, err := HashPasswordV1("test123")
tst.AssertNoErr(t, err)
tst.AssertTrue(t, ph.Valid())
tst.AssertFalse(t, ph.HasTOTP())
tst.AssertTrue(t, ph.NeedsPasswordUpgrade())
tst.AssertTrue(t, ph.Verify("test123", nil))
tst.AssertFalse(t, ph.Verify("test124", nil))
ph, err = ph.Upgrade("test123")
tst.AssertNoErr(t, err)
tst.AssertTrue(t, ph.Valid())
tst.AssertFalse(t, ph.HasTOTP())
tst.AssertFalse(t, ph.NeedsPasswordUpgrade())
tst.AssertTrue(t, ph.Verify("test123", nil))
tst.AssertFalse(t, ph.Verify("test124", nil))
}
func TestPassHashUpgrade_V2(t *testing.T) {
ph, err := HashPasswordV2("test123")
tst.AssertNoErr(t, err)
tst.AssertTrue(t, ph.Valid())
tst.AssertFalse(t, ph.HasTOTP())
tst.AssertTrue(t, ph.NeedsPasswordUpgrade())
tst.AssertTrue(t, ph.Verify("test123", nil))
tst.AssertFalse(t, ph.Verify("test124", nil))
ph, err = ph.Upgrade("test123")
tst.AssertNoErr(t, err)
tst.AssertTrue(t, ph.Valid())
tst.AssertFalse(t, ph.HasTOTP())
tst.AssertFalse(t, ph.NeedsPasswordUpgrade())
tst.AssertTrue(t, ph.Verify("test123", nil))
tst.AssertFalse(t, ph.Verify("test124", nil))
}
func TestPassHashUpgrade_V3(t *testing.T) {
ph, err := HashPasswordV3("test123", nil)
tst.AssertNoErr(t, err)
tst.AssertTrue(t, ph.Valid())
tst.AssertFalse(t, ph.HasTOTP())
tst.AssertTrue(t, ph.NeedsPasswordUpgrade())
tst.AssertTrue(t, ph.Verify("test123", nil))
tst.AssertFalse(t, ph.Verify("test124", nil))
ph, err = ph.Upgrade("test123")
tst.AssertNoErr(t, err)
tst.AssertTrue(t, ph.Valid())
tst.AssertFalse(t, ph.HasTOTP())
tst.AssertFalse(t, ph.NeedsPasswordUpgrade())
tst.AssertTrue(t, ph.Verify("test123", nil))
tst.AssertFalse(t, ph.Verify("test124", nil))
}
func TestPassHashUpgrade_V3_TOTP(t *testing.T) {
sec, err := totpext.GenerateSecret()
tst.AssertNoErr(t, err)
ph, err := HashPasswordV3("test123", sec)
tst.AssertNoErr(t, err)
tst.AssertTrue(t, ph.Valid())
tst.AssertTrue(t, ph.HasTOTP())
tst.AssertTrue(t, ph.NeedsPasswordUpgrade())
tst.AssertFalse(t, ph.Verify("test123", nil))
tst.AssertFalse(t, ph.Verify("test124", nil))
tst.AssertTrue(t, ph.Verify("test123", langext.Ptr(totpext.TOTP(sec))))
tst.AssertFalse(t, ph.Verify("test124", nil))
ph, err = ph.Upgrade("test123")
tst.AssertNoErr(t, err)
tst.AssertTrue(t, ph.Valid())
tst.AssertTrue(t, ph.HasTOTP())
tst.AssertFalse(t, ph.NeedsPasswordUpgrade())
tst.AssertFalse(t, ph.Verify("test123", nil))
tst.AssertFalse(t, ph.Verify("test124", nil))
tst.AssertTrue(t, ph.Verify("test123", langext.Ptr(totpext.TOTP(sec))))
tst.AssertFalse(t, ph.Verify("test124", nil))
}
func TestPassHashUpgrade_V4(t *testing.T) {
ph, err := HashPasswordV4("test123", nil)
tst.AssertNoErr(t, err)
tst.AssertTrue(t, ph.Valid())
tst.AssertFalse(t, ph.HasTOTP())
tst.AssertTrue(t, ph.NeedsPasswordUpgrade())
tst.AssertTrue(t, ph.Verify("test123", nil))
tst.AssertFalse(t, ph.Verify("test124", nil))
ph, err = ph.Upgrade("test123")
tst.AssertNoErr(t, err)
tst.AssertTrue(t, ph.Valid())
tst.AssertFalse(t, ph.HasTOTP())
tst.AssertFalse(t, ph.NeedsPasswordUpgrade())
tst.AssertTrue(t, ph.Verify("test123", nil))
tst.AssertFalse(t, ph.Verify("test124", nil))
}
func TestPassHashUpgrade_V4_TOTP(t *testing.T) {
sec, err := totpext.GenerateSecret()
tst.AssertNoErr(t, err)
ph, err := HashPasswordV4("test123", sec)
tst.AssertNoErr(t, err)
tst.AssertTrue(t, ph.Valid())
tst.AssertTrue(t, ph.HasTOTP())
tst.AssertTrue(t, ph.NeedsPasswordUpgrade())
tst.AssertFalse(t, ph.Verify("test123", nil))
tst.AssertFalse(t, ph.Verify("test124", nil))
tst.AssertTrue(t, ph.Verify("test123", langext.Ptr(totpext.TOTP(sec))))
tst.AssertFalse(t, ph.Verify("test124", nil))
ph, err = ph.Upgrade("test123")
tst.AssertNoErr(t, err)
tst.AssertTrue(t, ph.Valid())
tst.AssertTrue(t, ph.HasTOTP())
tst.AssertFalse(t, ph.NeedsPasswordUpgrade())
tst.AssertFalse(t, ph.Verify("test123", nil))
tst.AssertFalse(t, ph.Verify("test124", nil))
tst.AssertTrue(t, ph.Verify("test123", langext.Ptr(totpext.TOTP(sec))))
tst.AssertFalse(t, ph.Verify("test124", nil))
}

423
exerr/builder.go Normal file
View File

@@ -0,0 +1,423 @@
package exerr
import (
"bytes"
"context"
"encoding/json"
"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/langext"
"net/http"
"os"
"runtime/debug"
"strings"
"time"
)
//
// ==== USAGE =====
//
// If some method returns an error _always wrap it into an exerror:
// value, err := do_something(..)
// if err != nil {
// return nil, exerror.Wrap(err, "do something failed").Build()
// }
//
// If possible add metadata to the error (eg the id that was not found, ...), the methods are the same as in zerolog
// return nil, exerror.Wrap(err, "do something failed").Str("someid", id).Int("count", in.Count).Build()
//
// You can change the errortype with `.User()` and `.System()` (User-errors are 400 and System-errors 500)
// You can also manually set the statuscode with `.WithStatuscode(http.NotFound)`
// You can set the type with `WithType(..)`
//
// New Errors (that don't wrap an existing err object) are created with New
// return nil, exerror.New(exerror.TypeInternal, "womethign wen horrible wrong").Build()
// You can eitehr use an existing ErrorType, the "catch-all" ErrInternal, or add you own ErrType in consts.go
//
// All errors should be handled one of the following four ways:
// - return the error to the caller and let him handle it:
// (also auto-prints the error to the log)
// => Wrap/New + Build
// - Print the error
// (also auto-sends it to the error-service)
// This is useful for errors that happen asynchron or are non-fatal for the current request
// => Wrap/New + Print
// - Return the error to the Rest-API caller
// (also auto-prints the error to the log)
// (also auto-sends it to the error-service)
// => Wrap/New + Output
// - Print and stop the service
// (also auto-sends it to the error-service)
// => Wrap/New + Fatal
//
var stackSkipLogger zerolog.Logger
func init() {
cw := zerolog.ConsoleWriter{
Out: os.Stdout,
TimeFormat: "2006-01-02 15:04:05 Z07:00",
}
multi := zerolog.MultiLevelWriter(cw)
stackSkipLogger = zerolog.New(multi).With().Timestamp().CallerWithSkipFrameCount(4).Logger()
}
type Builder struct {
errorData *ExErr
containsGinData bool
}
func Get(err error) *Builder {
return &Builder{errorData: FromError(err)}
}
func New(t ErrorType, msg string) *Builder {
return &Builder{errorData: newExErr(CatSystem, t, msg)}
}
func Wrap(err error, msg string) *Builder {
if !pkgconfig.RecursiveErrors {
v := FromError(err)
v.Message = msg
return &Builder{errorData: v}
}
return &Builder{errorData: wrapExErr(FromError(err), msg, CatWrap, 1)}
}
// ----------------------------------------------------------------------------
func (b *Builder) WithType(t ErrorType) *Builder {
b.errorData.Type = t
return b
}
func (b *Builder) WithStatuscode(status int) *Builder {
b.errorData.StatusCode = &status
return b
}
func (b *Builder) WithMessage(msg string) *Builder {
b.errorData.Message = msg
return b
}
// ----------------------------------------------------------------------------
// Err changes the Severity to ERROR (default)
// The error will be:
//
// - On Build():
//
// - Short-Logged as Err
//
// - On Print():
//
// - Logged as Err
//
// - Send to the error-service
//
// - On Output():
//
// - Logged as Err
//
// - Send to the error-service
func (b *Builder) Err() *Builder {
b.errorData.Severity = SevErr
return b
}
// Warn changes the Severity to WARN
// The error will be:
//
// - On Build():
//
// - -(nothing)-
//
// - On Print():
//
// - Short-Logged as Warn
//
// - On Output():
//
// - Logged as Warn
func (b *Builder) Warn() *Builder {
b.errorData.Severity = SevWarn
return b
}
// Info changes the Severity to INFO
// The error will be:
//
// - On Build():
//
// - -(nothing)-
//
// - On Print():
//
// - -(nothing)-
//
// - On Output():
//
// - -(nothing)-
func (b *Builder) Info() *Builder {
b.errorData.Severity = SevInfo
return b
}
// ----------------------------------------------------------------------------
// User sets the Category to CatUser
//
// Errors with category
func (b *Builder) User() *Builder {
b.errorData.Category = CatUser
return b
}
func (b *Builder) System() *Builder {
b.errorData.Category = CatSystem
return b
}
// ----------------------------------------------------------------------------
func (b *Builder) Id(key string, val fmt.Stringer) *Builder {
return b.addMeta(key, MDTID, newIDWrap(val))
}
func (b *Builder) StrPtr(key string, val *string) *Builder {
return b.addMeta(key, MDTStringPtr, val)
}
func (b *Builder) Str(key string, val string) *Builder {
return b.addMeta(key, MDTString, val)
}
func (b *Builder) Int(key string, val int) *Builder {
return b.addMeta(key, MDTInt, val)
}
func (b *Builder) Int8(key string, val int8) *Builder {
return b.addMeta(key, MDTInt8, val)
}
func (b *Builder) Int16(key string, val int16) *Builder {
return b.addMeta(key, MDTInt16, val)
}
func (b *Builder) Int32(key string, val int32) *Builder {
return b.addMeta(key, MDTInt32, val)
}
func (b *Builder) Int64(key string, val int64) *Builder {
return b.addMeta(key, MDTInt64, val)
}
func (b *Builder) Float32(key string, val float32) *Builder {
return b.addMeta(key, MDTFloat32, val)
}
func (b *Builder) Float64(key string, val float64) *Builder {
return b.addMeta(key, MDTFloat64, val)
}
func (b *Builder) Bool(key string, val bool) *Builder {
return b.addMeta(key, MDTBool, val)
}
func (b *Builder) Bytes(key string, val []byte) *Builder {
return b.addMeta(key, MDTBytes, val)
}
func (b *Builder) ObjectID(key string, val primitive.ObjectID) *Builder {
return b.addMeta(key, MDTObjectID, val)
}
func (b *Builder) Time(key string, val time.Time) *Builder {
return b.addMeta(key, MDTTime, val)
}
func (b *Builder) Dur(key string, val time.Duration) *Builder {
return b.addMeta(key, MDTDuration, val)
}
func (b *Builder) Strs(key string, val []string) *Builder {
return b.addMeta(key, MDTStringArray, val)
}
func (b *Builder) Ints(key string, val []int) *Builder {
return b.addMeta(key, MDTIntArray, val)
}
func (b *Builder) Ints32(key string, val []int32) *Builder {
return b.addMeta(key, MDTInt32Array, val)
}
func (b *Builder) Type(key string, cls interface{}) *Builder {
return b.addMeta(key, MDTString, fmt.Sprintf("%T", cls))
}
func (b *Builder) Interface(key string, val interface{}) *Builder {
return b.addMeta(key, MDTAny, newAnyWrap(val))
}
func (b *Builder) Any(key string, val any) *Builder {
return b.addMeta(key, MDTAny, newAnyWrap(val))
}
func (b *Builder) Stack() *Builder {
return b.addMeta("@Stack", MDTString, string(debug.Stack()))
}
func (b *Builder) Errs(key string, val []error) *Builder {
for i, valerr := range val {
b.addMeta(fmt.Sprintf("%v[%v]", key, i), MDTString, Get(valerr).errorData.FormatLog(LogPrintFull))
}
return b
}
func (b *Builder) GinReq(ctx context.Context, g *gin.Context, req *http.Request) *Builder {
if v := ctx.Value("start_timestamp"); v != nil {
if t, ok := v.(time.Time); ok {
b.Time("ctx.startTimestamp", t)
b.Time("ctx.endTimestamp", time.Now())
}
}
b.Str("gin.method", req.Method)
b.Str("gin.path", g.FullPath())
b.Str("gin.header", formatHeader(g.Request.Header))
if req.URL != nil {
b.Str("gin.url", req.URL.String())
}
if ctxVal := g.GetString("apiversion"); ctxVal != "" {
b.Str("gin.context.apiversion", ctxVal)
}
if ctxVal := g.GetString("uid"); ctxVal != "" {
b.Str("gin.context.uid", ctxVal)
}
if ctxVal := g.GetString("fcmId"); ctxVal != "" {
b.Str("gin.context.fcmid", ctxVal)
}
if ctxVal := g.GetString("reqid"); ctxVal != "" {
b.Str("gin.context.reqid", ctxVal)
}
if req.Method != "GET" && req.Body != nil && req.Header.Get("Content-Type") == "application/json" {
if brc, ok := req.Body.(dataext.BufferedReadCloser); ok {
if bin, err := brc.BufferedAll(); err == nil {
if len(bin) < 16*1024 {
var prettyJSON bytes.Buffer
err = json.Indent(&prettyJSON, bin, "", " ")
if err == nil {
b.Str("gin.body", string(prettyJSON.Bytes()))
} else {
b.Bytes("gin.body", bin)
}
} else {
b.Str("gin.body", fmt.Sprintf("[[%v bytes]]", len(bin)))
}
}
}
}
b.containsGinData = true
return b
}
func formatHeader(header map[string][]string) string {
ml := 1
for k, _ := range header {
if len(k) > ml {
ml = len(k)
}
}
r := ""
for k, v := range header {
if r != "" {
r += "\n"
}
for _, hval := range v {
value := hval
value = strings.ReplaceAll(value, "\n", "\\n")
value = strings.ReplaceAll(value, "\r", "\\r")
value = strings.ReplaceAll(value, "\t", "\\t")
r += langext.StrPadRight(k, " ", ml) + " := " + value
}
}
return r
}
// ----------------------------------------------------------------------------
// Build creates a new error, ready to pass up the stack
// If the errors is not SevWarn or SevInfo it gets also logged (in short form, without stacktrace) onto stdout
func (b *Builder) Build() error {
warnOnPkgConfigNotInitialized()
if pkgconfig.ZeroLogErrTraces && (b.errorData.Severity == SevErr || b.errorData.Severity == SevFatal) {
b.errorData.ShortLog(stackSkipLogger.Error())
} else if pkgconfig.ZeroLogAllTraces {
b.errorData.ShortLog(stackSkipLogger.Error())
}
b.CallListener(MethodBuild)
return b.errorData
}
// Output prints the error onto the gin stdout.
// The error also gets printed to stdout/stderr
// If the error is SevErr|SevFatal we also send it to the error-service
func (b *Builder) Output(ctx context.Context, g *gin.Context) {
if !b.containsGinData && g.Request != nil {
// Auto-Add gin metadata if the caller hasn't already done it
b.GinReq(ctx, g, g.Request)
}
b.errorData.Output(g)
if b.errorData.Severity == SevErr || b.errorData.Severity == SevFatal {
b.errorData.Log(stackSkipLogger.Error())
} else if b.errorData.Severity == SevWarn {
b.errorData.Log(stackSkipLogger.Warn())
}
b.CallListener(MethodOutput)
}
// Print prints the error
// If the error is SevErr we also send it to the error-service
func (b *Builder) Print() {
if b.errorData.Severity == SevErr || b.errorData.Severity == SevFatal {
b.errorData.Log(stackSkipLogger.Error())
} else if b.errorData.Severity == SevWarn {
b.errorData.ShortLog(stackSkipLogger.Warn())
}
b.CallListener(MethodPrint)
}
func (b *Builder) Format(level LogPrintLevel) string {
return b.errorData.FormatLog(level)
}
// Fatal prints the error and terminates the program
// If the error is SevErr we also send it to the error-service
func (b *Builder) Fatal() {
b.errorData.Severity = SevFatal
b.errorData.Log(stackSkipLogger.WithLevel(zerolog.FatalLevel))
b.CallListener(MethodFatal)
os.Exit(1)
}
// ----------------------------------------------------------------------------
func (b *Builder) addMeta(key string, mdtype metaDataType, val interface{}) *Builder {
b.errorData.Meta.add(key, mdtype, val)
return b
}

201
exerr/constructor.go Normal file
View File

@@ -0,0 +1,201 @@
package exerr
import (
"encoding/json"
"fmt"
"go.mongodb.org/mongo-driver/bson/primitive"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"reflect"
"time"
)
var reflectTypeStr = reflect.TypeOf("")
func FromError(err error) *ExErr {
if verr, ok := err.(*ExErr); ok {
// A simple ExErr
return verr
}
// A foreign error (eg a MongoDB exception)
return &ExErr{
UniqueID: newID(),
Category: CatForeign,
Type: TypeInternal,
Severity: SevErr,
Timestamp: time.Time{},
StatusCode: nil,
Message: err.Error(),
WrappedErrType: fmt.Sprintf("%T", err),
Caller: "",
OriginalError: nil,
Meta: getForeignMeta(err),
}
}
func newExErr(cat ErrorCategory, errtype ErrorType, msg string) *ExErr {
return &ExErr{
UniqueID: newID(),
Category: cat,
Type: errtype,
Severity: SevErr,
Timestamp: time.Now(),
StatusCode: nil,
Message: msg,
WrappedErrType: "",
Caller: callername(2),
OriginalError: nil,
Meta: make(map[string]MetaValue),
}
}
func wrapExErr(e *ExErr, msg string, cat ErrorCategory, stacktraceskip int) *ExErr {
return &ExErr{
UniqueID: newID(),
Category: cat,
Type: TypeWrap,
Severity: SevErr,
Timestamp: time.Now(),
StatusCode: e.StatusCode,
Message: msg,
WrappedErrType: "",
Caller: callername(1 + stacktraceskip),
OriginalError: e,
Meta: make(map[string]MetaValue),
}
}
func getForeignMeta(err error) (mm MetaMap) {
mm = make(map[string]MetaValue)
defer func() {
if panicerr := recover(); panicerr != nil {
New(TypePanic, "Panic while trying to get foreign meta").
Str("source", err.Error()).
Interface("panic-object", panicerr).
Stack().
Print()
}
}()
rval := reflect.ValueOf(err)
if rval.Kind() == reflect.Interface || rval.Kind() == reflect.Ptr {
rval = reflect.ValueOf(err).Elem()
}
mm.add("foreign.errortype", MDTString, rval.Type().String())
for k, v := range addMetaPrefix("foreign", getReflectedMetaValues(err, 8)) {
mm[k] = v
}
return mm
}
func getReflectedMetaValues(value interface{}, remainingDepth int) map[string]MetaValue {
if remainingDepth <= 0 {
return map[string]MetaValue{}
}
if langext.IsNil(value) {
return map[string]MetaValue{"": {DataType: MDTNil, Value: nil}}
}
rval := reflect.ValueOf(value)
if rval.Type().Kind() == reflect.Ptr {
if rval.IsNil() {
return map[string]MetaValue{"*": {DataType: MDTNil, Value: nil}}
}
elem := rval.Elem()
return addMetaPrefix("*", getReflectedMetaValues(elem.Interface(), remainingDepth-1))
}
if !rval.CanInterface() {
return map[string]MetaValue{"": {DataType: MDTString, Value: "<<no-interface>>"}}
}
raw := rval.Interface()
switch ifraw := raw.(type) {
case time.Time:
return map[string]MetaValue{"": {DataType: MDTTime, Value: ifraw}}
case time.Duration:
return map[string]MetaValue{"": {DataType: MDTDuration, Value: ifraw}}
case int:
return map[string]MetaValue{"": {DataType: MDTInt, Value: ifraw}}
case int8:
return map[string]MetaValue{"": {DataType: MDTInt8, Value: ifraw}}
case int16:
return map[string]MetaValue{"": {DataType: MDTInt16, Value: ifraw}}
case int32:
return map[string]MetaValue{"": {DataType: MDTInt32, Value: ifraw}}
case int64:
return map[string]MetaValue{"": {DataType: MDTInt64, Value: ifraw}}
case string:
return map[string]MetaValue{"": {DataType: MDTString, Value: ifraw}}
case bool:
return map[string]MetaValue{"": {DataType: MDTBool, Value: ifraw}}
case []byte:
return map[string]MetaValue{"": {DataType: MDTBytes, Value: ifraw}}
case float32:
return map[string]MetaValue{"": {DataType: MDTFloat32, Value: ifraw}}
case float64:
return map[string]MetaValue{"": {DataType: MDTFloat64, Value: ifraw}}
case []int:
return map[string]MetaValue{"": {DataType: MDTIntArray, Value: ifraw}}
case []int32:
return map[string]MetaValue{"": {DataType: MDTInt32Array, Value: ifraw}}
case primitive.ObjectID:
return map[string]MetaValue{"": {DataType: MDTObjectID, Value: ifraw}}
case []string:
return map[string]MetaValue{"": {DataType: MDTStringArray, Value: ifraw}}
}
if rval.Type().Kind() == reflect.Struct {
m := make(map[string]MetaValue)
for i := 0; i < rval.NumField(); i++ {
fieldtype := rval.Type().Field(i)
fieldname := fieldtype.Name
if fieldtype.IsExported() {
for k, v := range addMetaPrefix(fieldname, getReflectedMetaValues(rval.Field(i).Interface(), remainingDepth-1)) {
m[k] = v
}
}
}
return m
}
if rval.Type().ConvertibleTo(reflectTypeStr) {
return map[string]MetaValue{"": {DataType: MDTString, Value: rval.Convert(reflectTypeStr).String()}}
}
jsonval, err := json.Marshal(value)
if err != nil {
panic(err) // gets recovered later up
}
return map[string]MetaValue{"": {DataType: MDTString, Value: string(jsonval)}}
}
func addMetaPrefix(prefix string, m map[string]MetaValue) map[string]MetaValue {
if len(m) == 1 {
for k, v := range m {
if k == "" {
return map[string]MetaValue{prefix: v}
}
}
}
r := make(map[string]MetaValue, len(m))
for k, v := range m {
r[prefix+"."+k] = v
}
return r
}

68
exerr/data.go Normal file
View File

@@ -0,0 +1,68 @@
package exerr
import (
"gogs.mikescher.com/BlackForestBytes/goext/langext"
)
type ErrorCategory struct{ Category string }
var (
CatWrap = ErrorCategory{"Wrap"} // The error is simply wrapping another error (e.g. when a grpc call returns an error)
CatSystem = ErrorCategory{"System"} // An internal system error (e.g. connection to db failed)
CatUser = ErrorCategory{"User"} // The user (the API caller) did something wrong (e.g. he has no permissions to do this)
CatForeign = ErrorCategory{"Foreign"} // A foreign error that some component threw (e.g. an unknown mongodb error), happens if we call Wrap(..) on an non-bmerror value
)
//goland:noinspection GoUnusedGlobalVariable
var AllCategories = []ErrorCategory{CatWrap, CatSystem, CatUser, CatForeign}
type ErrorSeverity struct{ Severity string }
var (
SevTrace = ErrorSeverity{"Trace"}
SevDebug = ErrorSeverity{"Debug"}
SevInfo = ErrorSeverity{"Info"}
SevWarn = ErrorSeverity{"Warn"}
SevErr = ErrorSeverity{"Err"}
SevFatal = ErrorSeverity{"Fatal"}
)
//goland:noinspection GoUnusedGlobalVariable
var AllSeverities = []ErrorSeverity{SevTrace, SevDebug, SevInfo, SevWarn, SevErr, SevFatal}
type ErrorType struct {
Key string
DefaultStatusCode *int
}
//goland:noinspection GoUnusedGlobalVariable
var (
TypeInternal = ErrorType{"INTERNAL_ERROR", langext.Ptr(500)}
TypePanic = ErrorType{"PANIC", langext.Ptr(500)}
TypeNotImplemented = ErrorType{"NOT_IMPLEMENTED", langext.Ptr(500)}
TypeWrap = ErrorType{"Wrap", nil}
TypeBindFailURI = ErrorType{"BINDFAIL_URI", langext.Ptr(400)}
TypeBindFailQuery = ErrorType{"BINDFAIL_QUERY", langext.Ptr(400)}
TypeBindFailJSON = ErrorType{"BINDFAIL_JSON", langext.Ptr(400)}
TypeBindFailFormData = ErrorType{"BINDFAIL_FORMDATA", langext.Ptr(400)}
TypeBindFailHeader = ErrorType{"BINDFAIL_HEADER", langext.Ptr(400)}
TypeUnauthorized = ErrorType{"UNAUTHORIZED", langext.Ptr(401)}
TypeAuthFailed = ErrorType{"AUTH_FAILED", langext.Ptr(401)}
// other values come from pkgconfig
)
func NewType(key string, defStatusCode *int) ErrorType {
return ErrorType{key, defStatusCode}
}
type LogPrintLevel string
const (
LogPrintFull LogPrintLevel = "Full"
LogPrintOverview LogPrintLevel = "Overview"
LogPrintShort LogPrintLevel = "Short"
)

76
exerr/errinit.go Normal file
View File

@@ -0,0 +1,76 @@
package exerr
import (
"fmt"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
)
type ErrorPackageConfig struct {
ZeroLogErrTraces bool // autom print zerolog logs on .Build() (for SevErr and SevFatal)
ZeroLogAllTraces bool // autom print zerolog logs on .Build() (for all Severities)
RecursiveErrors bool // errors contains their Origin-Error
ExtendedGinOutput bool // Log extended data (trace, meta, ...) to gin in err.Output()
ExtendGinOutput func(err *ExErr, json map[string]any) // (Optionally) extend the gin output with more fields
ExtendGinDataOutput func(err *ExErr, depth int, json map[string]any) // (Optionally) extend the gin `__data` output with more fields
}
type ErrorPackageConfigInit struct {
ZeroLogErrTraces bool
ZeroLogAllTraces bool
RecursiveErrors bool
ExtendedGinOutput bool
ExtendGinOutput func(err *ExErr, json map[string]any)
ExtendGinDataOutput func(err *ExErr, depth int, json map[string]any)
}
var initialized = false
var pkgconfig = ErrorPackageConfig{
ZeroLogErrTraces: true,
ZeroLogAllTraces: false,
RecursiveErrors: true,
ExtendedGinOutput: false,
ExtendGinOutput: func(err *ExErr, json map[string]any) {},
ExtendGinDataOutput: func(err *ExErr, depth int, json map[string]any) {},
}
// Init initializes the exerr packages
// Must be called at the program start, before (!) any errors
// Is not thread-safe
func Init(cfg ErrorPackageConfigInit) {
if initialized {
panic("Cannot re-init error package")
}
ego := func(err *ExErr, json map[string]any) {}
egdo := func(err *ExErr, depth int, json map[string]any) {}
if cfg.ExtendGinOutput != nil {
ego = cfg.ExtendGinOutput
}
if cfg.ExtendGinDataOutput != nil {
egdo = cfg.ExtendGinDataOutput
}
pkgconfig = ErrorPackageConfig{
ZeroLogErrTraces: cfg.ZeroLogErrTraces,
ZeroLogAllTraces: cfg.ZeroLogAllTraces,
RecursiveErrors: cfg.RecursiveErrors,
ExtendedGinOutput: cfg.ExtendedGinOutput,
ExtendGinOutput: ego,
ExtendGinDataOutput: egdo,
}
initialized = true
}
func warnOnPkgConfigNotInitialized() {
if !initialized {
fmt.Printf("\n")
fmt.Printf("%s\n", langext.StrRepeat("=", 80))
fmt.Printf("%s\n", "[WARNING] exerr package used without initializiation")
fmt.Printf("%s\n", " call exerr.Init() in your main() function")
fmt.Printf("%s\n", langext.StrRepeat("=", 80))
fmt.Printf("\n")
}
}

209
exerr/exerr.go Normal file
View File

@@ -0,0 +1,209 @@
package exerr
import (
"github.com/rs/xid"
"github.com/rs/zerolog"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"strings"
"time"
)
type ExErr struct {
UniqueID string `json:"uniqueID"`
Timestamp time.Time `json:"timestamp"`
Category ErrorCategory `json:"category"`
Severity ErrorSeverity `json:"severity"`
Type ErrorType `json:"type"`
StatusCode *int `json:"statusCode"`
Message string `json:"message"`
WrappedErrType string `json:"wrappedErrType"`
Caller string `json:"caller"`
OriginalError *ExErr `json:"originalError"`
Meta MetaMap `json:"meta"`
}
func (ee *ExErr) Error() string {
return ee.Message
}
// Unwrap must be implemented so that some error.XXX methods work
func (ee *ExErr) Unwrap() error {
return ee.OriginalError
}
// Is must be implemented so that error.Is(x) works
func (ee *ExErr) Is(e error) bool {
return IsFrom(ee, e)
}
func (ee *ExErr) Log(evt *zerolog.Event) {
evt.Msg(ee.FormatLog(LogPrintFull))
}
func (ee *ExErr) FormatLog(lvl LogPrintLevel) string {
if lvl == LogPrintShort {
msg := ee.Message
if ee.OriginalError != nil && ee.OriginalError.Category == CatForeign {
msg = msg + " (" + strings.ReplaceAll(ee.OriginalError.Message, "\n", " ") + ")"
}
if ee.Type != TypeWrap {
return "[" + ee.Type.Key + "] " + msg
} else {
return msg
}
} else if lvl == LogPrintOverview {
str := "[" + ee.RecursiveType().Key + "] <" + ee.UniqueID + "> " + strings.ReplaceAll(ee.RecursiveMessage(), "\n", " ") + "\n"
indent := ""
for curr := ee; curr != nil; curr = curr.OriginalError {
indent += " "
str += indent
str += "-> "
strmsg := strings.Trim(curr.Message, " \r\n\t")
if lbidx := strings.Index(curr.Message, "\n"); lbidx >= 0 {
strmsg = strmsg[0:lbidx]
}
strmsg = langext.StrLimit(strmsg, 61, "...")
str += strmsg
str += "\n"
}
return str
} else if lvl == LogPrintFull {
str := "[" + ee.RecursiveType().Key + "] <" + ee.UniqueID + "> " + strings.ReplaceAll(ee.RecursiveMessage(), "\n", " ") + "\n"
indent := ""
for curr := ee; curr != nil; curr = curr.OriginalError {
indent += " "
etype := ee.Type.Key
if ee.Type == TypeWrap {
etype = "~"
}
str += indent
str += "-> ["
str += etype
if curr.Category == CatForeign {
str += "|Foreign"
}
str += "] "
str += strings.ReplaceAll(curr.Message, "\n", " ")
if curr.Caller != "" {
str += " (@ "
str += curr.Caller
str += ")"
}
str += "\n"
if curr.Meta.Any() {
meta := indent + " {" + curr.Meta.FormatOneLine(240) + "}"
if len(meta) < 200 {
str += meta
str += "\n"
} else {
str += curr.Meta.FormatMultiLine(indent+" ", " ", 1024)
str += "\n"
}
}
}
return str
} else {
return "[?[" + ee.UniqueID + "]?]"
}
}
func (ee *ExErr) ShortLog(evt *zerolog.Event) {
ee.Meta.Apply(evt).Msg(ee.FormatLog(LogPrintShort))
}
// RecursiveMessage returns the message to show
// = first error (top-down) that is not wrapping/foreign/empty
func (ee *ExErr) RecursiveMessage() string {
for curr := ee; curr != nil; curr = curr.OriginalError {
if curr.Message != "" && curr.Category != CatWrap && curr.Category != CatForeign {
return curr.Message
}
}
// fallback to self
return ee.Message
}
// RecursiveType returns the statuscode to use
// = first error (top-down) that is not wrapping/empty
func (ee *ExErr) RecursiveType() ErrorType {
for curr := ee; curr != nil; curr = curr.OriginalError {
if curr.Type != TypeWrap {
return curr.Type
}
}
// fallback to self
return ee.Type
}
// RecursiveStatuscode returns the HTTP Statuscode to use
// = first error (top-down) that has a statuscode set
func (ee *ExErr) RecursiveStatuscode() *int {
for curr := ee; curr != nil; curr = curr.OriginalError {
if curr.StatusCode != nil {
return langext.Ptr(*curr.StatusCode)
}
}
return nil
}
// RecursiveCategory returns the ErrorCategory to use
// = first error (top-down) that has a statuscode set
func (ee *ExErr) RecursiveCategory() ErrorCategory {
for curr := ee; curr != nil; curr = curr.OriginalError {
if curr.Category != CatWrap {
return curr.Category
}
}
// fallback to <empty>
return ee.Category
}
// RecursiveMeta searches (top-down) for teh first error that has a meta value with teh specified key
// and returns its value (or nil)
func (ee *ExErr) RecursiveMeta(key string) *MetaValue {
for curr := ee; curr != nil; curr = curr.OriginalError {
if metaval, ok := curr.Meta[key]; ok {
return langext.Ptr(metaval)
}
}
return nil
}
func (ee *ExErr) Depth() int {
if ee.OriginalError == nil {
return 1
} else {
return ee.OriginalError.Depth() + 1
}
}
func newID() string {
return xid.New().String()
}

103
exerr/gin.go Normal file
View File

@@ -0,0 +1,103 @@
package exerr
import (
"github.com/gin-gonic/gin"
json "gogs.mikescher.com/BlackForestBytes/goext/gojson"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"net/http"
"time"
)
func (ee *ExErr) toJson(depth int, applyExtendListener bool) langext.H {
ginJson := langext.H{}
if ee.UniqueID != "" {
ginJson["id"] = ee.UniqueID
}
if ee.Category != CatWrap {
ginJson["category"] = ee.Category
}
if ee.Type != TypeWrap {
ginJson["type"] = ee.Type
}
if ee.StatusCode != nil {
ginJson["statuscode"] = ee.StatusCode
}
if ee.Message != "" {
ginJson["message"] = ee.Message
}
if ee.Caller != "" {
ginJson["caller"] = ee.Caller
}
if ee.Severity != SevErr {
ginJson["severity"] = ee.Severity
}
if ee.Timestamp != (time.Time{}) {
ginJson["time"] = ee.Timestamp.Format(time.RFC3339)
}
if ee.WrappedErrType != "" {
ginJson["wrappedErrType"] = ee.WrappedErrType
}
if ee.OriginalError != nil {
ginJson["original"] = ee.OriginalError.toJson(depth+1, applyExtendListener)
}
if applyExtendListener {
pkgconfig.ExtendGinDataOutput(ee, depth, ginJson)
}
return ginJson
}
// ToAPIJson converts the ExError to a json object
// (the same object as used in the Output(gin) method)
//
// Parameters:
// - [applyExtendListener]: if false the pkgconfig.ExtendGinOutput / pkgconfig.ExtendGinDataOutput will not be applied
// - [includeWrappedErrors]: if false we do not include the recursive/wrapped errors in `__data`
func (ee *ExErr) ToAPIJson(applyExtendListener bool, includeWrappedErrors bool) langext.H {
apiOutput := langext.H{
"errorid": ee.UniqueID,
"message": ee.RecursiveMessage(),
"errorcode": ee.RecursiveType().Key,
"category": ee.RecursiveCategory().Category,
}
if includeWrappedErrors {
apiOutput["__data"] = ee.toJson(0, applyExtendListener)
}
if applyExtendListener {
pkgconfig.ExtendGinOutput(ee, apiOutput)
}
return apiOutput
}
func (ee *ExErr) Output(g *gin.Context) {
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 := ee.ToAPIJson(true, pkgconfig.ExtendedGinOutput)
g.Render(statuscode, json.GoJsonRender{Data: ginOutput, NilSafeSlices: true, NilSafeMaps: true})
}

88
exerr/helper.go Normal file
View File

@@ -0,0 +1,88 @@
package exerr
import "fmt"
// IsType test if the supplied error is of the specified ErrorType.
func IsType(err error, errType ErrorType) bool {
if err == nil {
return false
}
bmerr := FromError(err)
for bmerr != nil {
if bmerr.Type == errType {
return true
}
bmerr = bmerr.OriginalError
}
return false
}
// IsFrom test if the supplied error stems originally from original
func IsFrom(e error, original error) bool {
if e == nil {
return false
}
//goland:noinspection GoDirectComparisonOfErrors
if e == original {
return true
}
bmerr := FromError(e)
for bmerr == nil {
return false
}
for curr := bmerr; curr != nil; curr = curr.OriginalError {
if curr.Category == CatForeign && curr.Message == original.Error() && curr.WrappedErrType == fmt.Sprintf("%T", original) {
return true
}
}
return false
}
// HasSourceMessage tests if the supplied error stems originally from an error with the message msg
func HasSourceMessage(e error, msg string) bool {
if e == nil {
return false
}
bmerr := FromError(e)
for bmerr == nil {
return false
}
for curr := bmerr; curr != nil; curr = curr.OriginalError {
if curr.OriginalError == nil && curr.Message == msg {
return true
}
}
return false
}
func MessageMatch(e error, matcher func(string) bool) bool {
if e == nil {
return false
}
if matcher(e.Error()) {
return true
}
bmerr := FromError(e)
for bmerr == nil {
return false
}
for curr := bmerr; curr != nil; curr = curr.OriginalError {
if matcher(curr.Message) {
return true
}
}
return false
}

37
exerr/listener.go Normal file
View File

@@ -0,0 +1,37 @@
package exerr
import (
"sync"
)
type Method string
const (
MethodOutput Method = "OUTPUT"
MethodPrint Method = "PRINT"
MethodBuild Method = "BUILD"
MethodFatal Method = "FATAL"
)
type Listener = func(method Method, v *ExErr)
var listenerLock = sync.Mutex{}
var listener = make([]Listener, 0)
func RegisterListener(l Listener) {
listenerLock.Lock()
defer listenerLock.Unlock()
listener = append(listener, l)
}
func (b *Builder) CallListener(m Method) {
valErr := b.errorData
listenerLock.Lock()
defer listenerLock.Unlock()
for _, v := range listener {
v(m, valErr)
}
}

651
exerr/meta.go Normal file
View File

@@ -0,0 +1,651 @@
package exerr
import (
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"github.com/rs/zerolog"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"strconv"
"strings"
"time"
)
// This is a buffed up map[string]any
// we also save type information of the map-values
// which allows us to deserialize them back into te correct types later
type MetaMap map[string]MetaValue
type metaDataType string
const (
MDTString metaDataType = "String"
MDTStringPtr metaDataType = "StringPtr"
MDTInt metaDataType = "Int"
MDTInt8 metaDataType = "Int8"
MDTInt16 metaDataType = "Int16"
MDTInt32 metaDataType = "Int32"
MDTInt64 metaDataType = "Int64"
MDTFloat32 metaDataType = "Float32"
MDTFloat64 metaDataType = "Float64"
MDTBool metaDataType = "Bool"
MDTBytes metaDataType = "Bytes"
MDTObjectID metaDataType = "ObjectID"
MDTTime metaDataType = "Time"
MDTDuration metaDataType = "Duration"
MDTStringArray metaDataType = "StringArr"
MDTIntArray metaDataType = "IntArr"
MDTInt32Array metaDataType = "Int32Arr"
MDTID metaDataType = "ID"
MDTAny metaDataType = "Interface"
MDTNil metaDataType = "Nil"
)
type MetaValue struct {
DataType metaDataType `json:"dataType"`
Value interface{} `json:"value"`
}
type metaValueSerialization struct {
DataType metaDataType `bson:"dataType"`
Value string `bson:"value"`
Raw interface{} `bson:"raw"`
}
func (v MetaValue) SerializeValue() (string, error) {
switch v.DataType {
case MDTString:
return v.Value.(string), nil
case MDTID:
return v.Value.(IDWrap).Serialize(), nil
case MDTAny:
return v.Value.(AnyWrap).Serialize(), nil
case MDTStringPtr:
if langext.IsNil(v.Value) {
return "#", nil
}
r := v.Value.(*string)
if r != nil {
return "*" + *r, nil
} else {
return "#", nil
}
case MDTInt:
return strconv.Itoa(v.Value.(int)), nil
case MDTInt8:
return strconv.FormatInt(int64(v.Value.(int8)), 10), nil
case MDTInt16:
return strconv.FormatInt(int64(v.Value.(int16)), 10), nil
case MDTInt32:
return strconv.FormatInt(int64(v.Value.(int32)), 10), nil
case MDTInt64:
return strconv.FormatInt(v.Value.(int64), 10), nil
case MDTFloat32:
return strconv.FormatFloat(float64(v.Value.(float32)), 'X', -1, 32), nil
case MDTFloat64:
return strconv.FormatFloat(v.Value.(float64), 'X', -1, 64), nil
case MDTBool:
if v.Value.(bool) {
return "true", nil
} else {
return "false", nil
}
case MDTBytes:
return hex.EncodeToString(v.Value.([]byte)), nil
case MDTObjectID:
return v.Value.(primitive.ObjectID).Hex(), nil
case MDTTime:
return strconv.FormatInt(v.Value.(time.Time).Unix(), 10) + "|" + strconv.FormatInt(int64(v.Value.(time.Time).Nanosecond()), 10), nil
case MDTDuration:
return v.Value.(time.Duration).String(), nil
case MDTStringArray:
if langext.IsNil(v.Value) {
return "#", nil
}
r, err := json.Marshal(v.Value.([]string))
if err != nil {
return "", err
}
return string(r), nil
case MDTIntArray:
if langext.IsNil(v.Value) {
return "#", nil
}
r, err := json.Marshal(v.Value.([]int))
if err != nil {
return "", err
}
return string(r), nil
case MDTInt32Array:
if langext.IsNil(v.Value) {
return "#", nil
}
r, err := json.Marshal(v.Value.([]int32))
if err != nil {
return "", err
}
return string(r), nil
case MDTNil:
return "", nil
}
return "", errors.New("Unknown type: " + string(v.DataType))
}
func (v MetaValue) ShortString(lim int) string {
switch v.DataType {
case MDTString:
r := strings.ReplaceAll(v.Value.(string), "\r", "")
r = strings.ReplaceAll(r, "\n", "\\n")
r = strings.ReplaceAll(r, "\t", "\\t")
return langext.StrLimit(r, lim, "...")
case MDTID:
return v.Value.(IDWrap).String()
case MDTAny:
return v.Value.(AnyWrap).String()
case MDTStringPtr:
if langext.IsNil(v.Value) {
return "<<null>>"
}
r := langext.CoalesceString(v.Value.(*string), "<<null>>")
r = strings.ReplaceAll(r, "\r", "")
r = strings.ReplaceAll(r, "\n", "\\n")
r = strings.ReplaceAll(r, "\t", "\\t")
return langext.StrLimit(r, lim, "...")
case MDTInt:
return strconv.Itoa(v.Value.(int))
case MDTInt8:
return strconv.FormatInt(int64(v.Value.(int8)), 10)
case MDTInt16:
return strconv.FormatInt(int64(v.Value.(int16)), 10)
case MDTInt32:
return strconv.FormatInt(int64(v.Value.(int32)), 10)
case MDTInt64:
return strconv.FormatInt(v.Value.(int64), 10)
case MDTFloat32:
return strconv.FormatFloat(float64(v.Value.(float32)), 'g', 4, 32)
case MDTFloat64:
return strconv.FormatFloat(v.Value.(float64), 'g', 4, 64)
case MDTBool:
return fmt.Sprintf("%v", v.Value.(bool))
case MDTBytes:
return langext.StrLimit(hex.EncodeToString(v.Value.([]byte)), lim, "...")
case MDTObjectID:
return v.Value.(primitive.ObjectID).Hex()
case MDTTime:
return v.Value.(time.Time).Format(time.RFC3339)
case MDTDuration:
return v.Value.(time.Duration).String()
case MDTStringArray:
if langext.IsNil(v.Value) {
return "<<null>>"
}
r, err := json.Marshal(v.Value.([]string))
if err != nil {
return "(err)"
}
return langext.StrLimit(string(r), lim, "...")
case MDTIntArray:
if langext.IsNil(v.Value) {
return "<<null>>"
}
r, err := json.Marshal(v.Value.([]int))
if err != nil {
return "(err)"
}
return langext.StrLimit(string(r), lim, "...")
case MDTInt32Array:
if langext.IsNil(v.Value) {
return "<<null>>"
}
r, err := json.Marshal(v.Value.([]int32))
if err != nil {
return "(err)"
}
return langext.StrLimit(string(r), lim, "...")
case MDTNil:
return "<<null>>"
}
return "(err)"
}
func (v MetaValue) Apply(key string, evt *zerolog.Event) *zerolog.Event {
switch v.DataType {
case MDTString:
return evt.Str(key, v.Value.(string))
case MDTID:
return evt.Str(key, v.Value.(IDWrap).Value)
case MDTAny:
if v.Value.(AnyWrap).IsError {
return evt.Str(key, "(err)")
} else {
return evt.Str(key, v.Value.(AnyWrap).Json)
}
case MDTStringPtr:
if langext.IsNil(v.Value) {
return evt.Str(key, "<<null>>")
}
return evt.Str(key, langext.CoalesceString(v.Value.(*string), "<<null>>"))
case MDTInt:
return evt.Int(key, v.Value.(int))
case MDTInt8:
return evt.Int8(key, v.Value.(int8))
case MDTInt16:
return evt.Int16(key, v.Value.(int16))
case MDTInt32:
return evt.Int32(key, v.Value.(int32))
case MDTInt64:
return evt.Int64(key, v.Value.(int64))
case MDTFloat32:
return evt.Float32(key, v.Value.(float32))
case MDTFloat64:
return evt.Float64(key, v.Value.(float64))
case MDTBool:
return evt.Bool(key, v.Value.(bool))
case MDTBytes:
return evt.Bytes(key, v.Value.([]byte))
case MDTObjectID:
return evt.Str(key, v.Value.(primitive.ObjectID).Hex())
case MDTTime:
return evt.Time(key, v.Value.(time.Time))
case MDTDuration:
return evt.Dur(key, v.Value.(time.Duration))
case MDTStringArray:
if langext.IsNil(v.Value) {
return evt.Strs(key, nil)
}
return evt.Strs(key, v.Value.([]string))
case MDTIntArray:
if langext.IsNil(v.Value) {
return evt.Ints(key, nil)
}
return evt.Ints(key, v.Value.([]int))
case MDTInt32Array:
if langext.IsNil(v.Value) {
return evt.Ints32(key, nil)
}
return evt.Ints32(key, v.Value.([]int32))
case MDTNil:
return evt.Str(key, "<<null>>")
}
return evt.Str(key, "(err)")
}
func (v MetaValue) MarshalJSON() ([]byte, error) {
str, err := v.SerializeValue()
if err != nil {
return nil, err
}
return json.Marshal(string(v.DataType) + ":" + str)
}
func (v *MetaValue) UnmarshalJSON(data []byte) error {
var str = ""
err := json.Unmarshal(data, &str)
if err != nil {
return err
}
split := strings.SplitN(str, ":", 2)
if len(split) != 2 {
return errors.New("failed to decode MetaValue: '" + str + "'")
}
return v.Deserialize(split[1], metaDataType(split[0]))
}
func (v MetaValue) MarshalBSON() ([]byte, error) {
serval, err := v.SerializeValue()
if err != nil {
return nil, Wrap(err, "failed to bson-marshal MetaValue (serialize)").Build()
}
// this is an kinda ugly hack - but serialization to mongodb and back can loose the correct type information....
bin, err := bson.Marshal(metaValueSerialization{
DataType: v.DataType,
Value: serval,
Raw: v.Value,
})
if err != nil {
return nil, Wrap(err, "failed to bson-marshal MetaValue (marshal)").Build()
}
return bin, nil
}
func (v *MetaValue) UnmarshalBSON(bytes []byte) error {
var serval metaValueSerialization
err := bson.Unmarshal(bytes, &serval)
if err != nil {
return Wrap(err, "failed to bson-unmarshal MetaValue (unmarshal)").Build()
}
err = v.Deserialize(serval.Value, serval.DataType)
if err != nil {
return Wrap(err, "failed to deserialize MetaValue from bson").Str("raw", serval.Value).Build()
}
return nil
}
func (v *MetaValue) Deserialize(value string, datatype metaDataType) error {
switch datatype {
case MDTString:
v.Value = value
v.DataType = datatype
return nil
case MDTID:
v.Value = deserializeIDWrap(value)
v.DataType = datatype
return nil
case MDTAny:
v.Value = deserializeAnyWrap(value)
v.DataType = datatype
return nil
case MDTStringPtr:
if len(value) <= 0 || (value[0] != '*' && value[0] != '#') {
return errors.New("Invalid StringPtr: " + value)
} else if value == "#" {
v.Value = nil
v.DataType = datatype
return nil
} else {
v.Value = langext.Ptr(value[1:])
v.DataType = datatype
return nil
}
case MDTInt:
pv, err := strconv.ParseInt(value, 10, 0)
if err != nil {
return err
}
v.Value = int(pv)
v.DataType = datatype
return nil
case MDTInt8:
pv, err := strconv.ParseInt(value, 10, 8)
if err != nil {
return err
}
v.Value = int8(pv)
v.DataType = datatype
return nil
case MDTInt16:
pv, err := strconv.ParseInt(value, 10, 16)
if err != nil {
return err
}
v.Value = int16(pv)
v.DataType = datatype
return nil
case MDTInt32:
pv, err := strconv.ParseInt(value, 10, 32)
if err != nil {
return err
}
v.Value = int32(pv)
v.DataType = datatype
return nil
case MDTInt64:
pv, err := strconv.ParseInt(value, 10, 64)
if err != nil {
return err
}
v.Value = pv
v.DataType = datatype
return nil
case MDTFloat32:
pv, err := strconv.ParseFloat(value, 64)
if err != nil {
return err
}
v.Value = float32(pv)
v.DataType = datatype
return nil
case MDTFloat64:
pv, err := strconv.ParseFloat(value, 64)
if err != nil {
return err
}
v.Value = pv
v.DataType = datatype
return nil
case MDTBool:
if value == "true" {
v.Value = true
v.DataType = datatype
return nil
}
if value == "false" {
v.Value = false
v.DataType = datatype
return nil
}
return errors.New("invalid bool value: " + value)
case MDTBytes:
r, err := hex.DecodeString(value)
if err != nil {
return err
}
v.Value = r
v.DataType = datatype
return nil
case MDTObjectID:
r, err := primitive.ObjectIDFromHex(value)
if err != nil {
return err
}
v.Value = r
v.DataType = datatype
return nil
case MDTTime:
ps := strings.Split(value, "|")
if len(ps) != 2 {
return errors.New("invalid time.time: " + value)
}
p1, err := strconv.ParseInt(ps[0], 10, 64)
if err != nil {
return err
}
p2, err := strconv.ParseInt(ps[1], 10, 32)
if err != nil {
return err
}
v.Value = time.Unix(p1, p2)
v.DataType = datatype
return nil
case MDTDuration:
r, err := time.ParseDuration(value)
if err != nil {
return err
}
v.Value = r
v.DataType = datatype
return nil
case MDTStringArray:
if value == "#" {
v.Value = nil
v.DataType = datatype
return nil
}
pj := make([]string, 0)
err := json.Unmarshal([]byte(value), &pj)
if err != nil {
return err
}
v.Value = pj
v.DataType = datatype
return nil
case MDTIntArray:
if value == "#" {
v.Value = nil
v.DataType = datatype
return nil
}
pj := make([]int, 0)
err := json.Unmarshal([]byte(value), &pj)
if err != nil {
return err
}
v.Value = pj
v.DataType = datatype
return nil
case MDTInt32Array:
if value == "#" {
v.Value = nil
v.DataType = datatype
return nil
}
pj := make([]int32, 0)
err := json.Unmarshal([]byte(value), &pj)
if err != nil {
return err
}
v.Value = pj
v.DataType = datatype
return nil
case MDTNil:
v.Value = nil
v.DataType = datatype
return nil
}
return errors.New("Unknown type: " + string(datatype))
}
func (v MetaValue) ValueString() string {
switch v.DataType {
case MDTString:
return v.Value.(string)
case MDTID:
return v.Value.(IDWrap).String()
case MDTAny:
return v.Value.(AnyWrap).String()
case MDTStringPtr:
if langext.IsNil(v.Value) {
return "<<null>>"
}
return langext.CoalesceString(v.Value.(*string), "<<null>>")
case MDTInt:
return strconv.Itoa(v.Value.(int))
case MDTInt8:
return strconv.FormatInt(int64(v.Value.(int8)), 10)
case MDTInt16:
return strconv.FormatInt(int64(v.Value.(int16)), 10)
case MDTInt32:
return strconv.FormatInt(int64(v.Value.(int32)), 10)
case MDTInt64:
return strconv.FormatInt(v.Value.(int64), 10)
case MDTFloat32:
return strconv.FormatFloat(float64(v.Value.(float32)), 'g', 4, 32)
case MDTFloat64:
return strconv.FormatFloat(v.Value.(float64), 'g', 4, 64)
case MDTBool:
return fmt.Sprintf("%v", v.Value.(bool))
case MDTBytes:
return hex.EncodeToString(v.Value.([]byte))
case MDTObjectID:
return v.Value.(primitive.ObjectID).Hex()
case MDTTime:
return v.Value.(time.Time).Format(time.RFC3339Nano)
case MDTDuration:
return v.Value.(time.Duration).String()
case MDTStringArray:
if langext.IsNil(v.Value) {
return "<<null>>"
}
r, err := json.MarshalIndent(v.Value.([]string), "", " ")
if err != nil {
return "(err)"
}
return string(r)
case MDTIntArray:
if langext.IsNil(v.Value) {
return "<<null>>"
}
r, err := json.MarshalIndent(v.Value.([]int), "", " ")
if err != nil {
return "(err)"
}
return string(r)
case MDTInt32Array:
if langext.IsNil(v.Value) {
return "<<null>>"
}
r, err := json.MarshalIndent(v.Value.([]int32), "", " ")
if err != nil {
return "(err)"
}
return string(r)
case MDTNil:
return "<<null>>"
}
return "(err)"
}
func (mm MetaMap) FormatOneLine(singleMaxLen int) string {
r := ""
i := 0
for key, val := range mm {
if i > 0 {
r += ", "
}
r += "\"" + key + "\""
r += ": "
r += "\"" + val.ShortString(singleMaxLen) + "\""
i++
}
return r
}
func (mm MetaMap) FormatMultiLine(indentFront string, indentKeys string, maxLenValue int) string {
r := ""
r += indentFront + "{" + "\n"
for key, val := range mm {
if key == "gin.body" {
continue
}
r += indentFront
r += indentKeys
r += "\"" + key + "\""
r += ": "
r += "\"" + val.ShortString(maxLenValue) + "\""
r += ",\n"
}
r += indentFront + "}"
return r
}
func (mm MetaMap) Any() bool {
return len(mm) > 0
}
func (mm MetaMap) Apply(evt *zerolog.Event) *zerolog.Event {
for key, val := range mm {
evt = val.Apply(key, evt)
}
return evt
}
func (mm MetaMap) add(key string, mdtype metaDataType, val interface{}) {
if _, ok := mm[key]; !ok {
mm[key] = MetaValue{DataType: mdtype, Value: val}
return
}
for i := 2; ; i++ {
realkey := key + "-" + strconv.Itoa(i)
if _, ok := mm[realkey]; !ok {
mm[realkey] = MetaValue{DataType: mdtype, Value: val}
return
}
}
}

14
exerr/stacktrace.go Normal file
View File

@@ -0,0 +1,14 @@
package exerr
import (
"fmt"
"runtime"
)
func callername(skip int) string {
pc := make([]uintptr, 15)
n := runtime.Callers(skip+2, pc)
frames := runtime.CallersFrames(pc[:n])
frame, _ := frames.Next()
return fmt.Sprintf("%s:%d %s", frame.File, frame.Line, frame.Function)
}

133
exerr/typeWrapper.go Normal file
View File

@@ -0,0 +1,133 @@
package exerr
import (
"encoding/json"
"fmt"
"github.com/rs/zerolog/log"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"strings"
)
//
// These are wrapper objects, because for some metadata-types we need to serialize a bit more complex data
// (eg thy actual type for ID objects, or the json representation for any types)
//
type IDWrap struct {
Type string
Value string
IsNil bool
}
func newIDWrap(val fmt.Stringer) IDWrap {
t := fmt.Sprintf("%T", val)
arr := strings.Split(t, ".")
if len(arr) > 0 {
t = arr[len(arr)-1]
}
if langext.IsNil(val) {
return IDWrap{Type: t, Value: "", IsNil: true}
}
v := val.String()
return IDWrap{Type: t, Value: v, IsNil: false}
}
func (w IDWrap) Serialize() string {
if w.IsNil {
return "!nil" + ":" + w.Type
}
return w.Type + ":" + w.Value
}
func (w IDWrap) String() string {
if w.IsNil {
return w.Type + "<<nil>>"
}
return w.Type + "(" + w.Value + ")"
}
func deserializeIDWrap(v string) IDWrap {
r := strings.SplitN(v, ":", 2)
if len(r) == 2 && r[0] == "!nil" {
return IDWrap{Type: r[1], Value: v, IsNil: true}
}
if len(r) == 0 {
return IDWrap{}
} else if len(r) == 1 {
return IDWrap{Type: "", Value: v, IsNil: false}
} else {
return IDWrap{Type: r[0], Value: r[1], IsNil: false}
}
}
type AnyWrap struct {
Type string
Json string
IsError bool
IsNil bool
}
func newAnyWrap(val any) (result AnyWrap) {
result = AnyWrap{Type: "", Json: "", IsError: true, IsNil: false} // ensure a return in case of recover()
defer func() {
if err := recover(); err != nil {
// send error should never crash our program
log.Error().Interface("err", err).Msg("Panic while trying to marshal anywrap ( bmerror.Interface )")
}
}()
t := fmt.Sprintf("%T", val)
if langext.IsNil(val) {
return AnyWrap{Type: t, Json: "", IsError: false, IsNil: true}
}
j, err := json.Marshal(val)
if err == nil {
return AnyWrap{Type: t, Json: string(j), IsError: false, IsNil: false}
} else {
return AnyWrap{Type: t, Json: "", IsError: true, IsNil: false}
}
}
func (w AnyWrap) Serialize() string {
if w.IsError {
return "ERR" + ":" + w.Type + ":" + w.Json
} else if w.IsNil {
return "NIL" + ":" + w.Type + ":" + w.Json
} else {
return "OK" + ":" + w.Type + ":" + w.Json
}
}
func (w AnyWrap) String() string {
if w.IsError {
return "(error)"
} else if w.IsNil {
return "(nil)"
} else {
return w.Json
}
}
func deserializeAnyWrap(v string) AnyWrap {
r := strings.SplitN(v, ":", 3)
if len(r) != 3 {
return AnyWrap{IsError: true, Type: "", Json: "", IsNil: false}
} else {
if r[0] == "OK" {
return AnyWrap{IsError: false, Type: r[1], Json: r[2], IsNil: false}
} else if r[0] == "ERR" {
return AnyWrap{IsError: true, Type: r[1], Json: r[2], IsNil: false}
} else if r[0] == "NIL" {
return AnyWrap{IsError: false, Type: r[1], Json: "", IsNil: true}
} else {
return AnyWrap{IsError: true, Type: "", Json: "", IsNil: false}
}
}
}

59
ginext/appContext.go Normal file
View File

@@ -0,0 +1,59 @@
package ginext
import (
"context"
"github.com/gin-gonic/gin"
"time"
)
type AppContext struct {
inner context.Context
cancelFunc context.CancelFunc
cancelled bool
GinContext *gin.Context
}
func CreateAppContext(g *gin.Context, innerCtx context.Context, cancelFn context.CancelFunc) *AppContext {
for key, value := range g.Keys {
innerCtx = context.WithValue(innerCtx, key, value)
}
return &AppContext{
inner: innerCtx,
cancelFunc: cancelFn,
cancelled: false,
GinContext: g,
}
}
func (ac *AppContext) Deadline() (deadline time.Time, ok bool) {
return ac.inner.Deadline()
}
func (ac *AppContext) Done() <-chan struct{} {
return ac.inner.Done()
}
func (ac *AppContext) Err() error {
return ac.inner.Err()
}
func (ac *AppContext) Value(key any) any {
return ac.inner.Value(key)
}
func (ac *AppContext) Set(key, value any) {
ac.inner = context.WithValue(ac.inner, key, value)
}
func (ac *AppContext) Cancel() {
ac.cancelled = true
ac.cancelFunc()
}
func (ac *AppContext) RequestURI() string {
if ac.GinContext != nil && ac.GinContext.Request != nil {
return ac.GinContext.Request.Method + " :: " + ac.GinContext.Request.RequestURI
} else {
return ""
}
}

23
ginext/commonHandler.go Normal file
View File

@@ -0,0 +1,23 @@
package ginext
import (
"net/http"
)
func RedirectFound(newuri string) WHandlerFunc {
return func(pctx PreContext) HTTPResponse {
return Redirect(http.StatusFound, newuri)
}
}
func RedirectTemporary(newuri string) WHandlerFunc {
return func(pctx PreContext) HTTPResponse {
return Redirect(http.StatusTemporaryRedirect, newuri)
}
}
func RedirectPermanent(newuri string) WHandlerFunc {
return func(pctx PreContext) HTTPResponse {
return Redirect(http.StatusPermanentRedirect, newuri)
}
}

View File

@@ -0,0 +1,14 @@
package ginext
import (
"github.com/gin-gonic/gin"
"gogs.mikescher.com/BlackForestBytes/goext/dataext"
)
func BodyBuffer() gin.HandlerFunc {
return func(g *gin.Context) {
if g.Request.Body != nil {
g.Request.Body = dataext.NewBufferedReadCloser(g.Request.Body)
}
}
}

21
ginext/cors.go Normal file
View File

@@ -0,0 +1,21 @@
package ginext
import (
"github.com/gin-gonic/gin"
"net/http"
)
func CorsMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
c.Writer.Header().Set("Access-Control-Allow-Methods", "OPTIONS, GET, POST, PUT, PATCH, DELETE, COUNT")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(http.StatusOK)
} else {
c.Next()
}
}
}

149
ginext/engine.go Normal file
View File

@@ -0,0 +1,149 @@
package ginext
import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog/log"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/mathext"
"net"
"net/http"
"strings"
"time"
)
type GinWrapper struct {
engine *gin.Engine
SuppressGinLogs bool
allowCors bool
ginDebug bool
bufferBody bool
requestTimeout time.Duration
routeSpecs []ginRouteSpec
}
type ginRouteSpec struct {
Method string
URL string
Middlewares []string
Handler string
}
// NewEngine creates a new (wrapped) ginEngine
// Parameters are:
// - [allowCors] Add cors handler to allow all CORS requests on the default http methods
// - [ginDebug] Set gin.debug to true (adds more logs)
// - [bufferBody] Buffers the input body stream, this way the ginext error handler can later include the whole request body
// - [timeout] The default handler timeout
func NewEngine(allowCors bool, ginDebug bool, bufferBody bool, timeout time.Duration) *GinWrapper {
engine := gin.New()
wrapper := &GinWrapper{
engine: engine,
SuppressGinLogs: false,
allowCors: allowCors,
ginDebug: ginDebug,
bufferBody: bufferBody,
requestTimeout: timeout,
}
engine.RedirectFixedPath = false
engine.RedirectTrailingSlash = false
if allowCors {
engine.Use(CorsMiddleware())
}
// do not debug-print routes
gin.DebugPrintRouteFunc = func(_, _, _ string, _ int) {}
if ginDebug {
gin.SetMode(gin.ReleaseMode)
ginlogger := gin.Logger()
engine.Use(func(context *gin.Context) {
if !wrapper.SuppressGinLogs {
ginlogger(context)
}
})
} else {
gin.SetMode(gin.DebugMode)
}
return wrapper
}
func (w *GinWrapper) ListenAndServeHTTP(addr string, postInit func(port string)) (chan error, *http.Server) {
w.DebugPrintRoutes()
httpserver := &http.Server{
Addr: addr,
Handler: w.engine,
}
errChan := make(chan error)
go func() {
ln, err := net.Listen("tcp", httpserver.Addr)
if err != nil {
errChan <- err
return
}
_, port, err := net.SplitHostPort(ln.Addr().String())
if err != nil {
errChan <- err
return
}
log.Info().Str("address", httpserver.Addr).Msg("HTTP-Server started on http://localhost:" + port)
if postInit != nil {
postInit(port) // the net.Listener a few lines above is at this point actually already buffering requests
}
errChan <- httpserver.Serve(ln)
}()
return errChan, httpserver
}
func (w *GinWrapper) DebugPrintRoutes() {
if !w.ginDebug {
return
}
lines := make([][4]string, 0)
pad := [4]int{0, 0, 0, 0}
for _, spec := range w.routeSpecs {
line := [4]string{
spec.Method,
spec.URL,
strings.Join(spec.Middlewares, " -> "),
spec.Handler,
}
lines = append(lines, line)
pad[0] = mathext.Max(pad[0], len(line[0]))
pad[1] = mathext.Max(pad[1], len(line[1]))
pad[2] = mathext.Max(pad[2], len(line[2]))
pad[3] = mathext.Max(pad[3], len(line[3]))
}
for _, line := range lines {
fmt.Printf("Gin-Route: %s %s --> %s --> %s\n",
langext.StrPadRight("["+line[0]+"]", " ", pad[0]+2),
langext.StrPadRight(line[1], " ", pad[1]),
langext.StrPadRight(line[2], " ", pad[2]),
langext.StrPadRight(line[3], " ", pad[3]))
}
}

39
ginext/funcWrapper.go Normal file
View File

@@ -0,0 +1,39 @@
package ginext
import (
"fmt"
"github.com/gin-gonic/gin"
"gogs.mikescher.com/BlackForestBytes/goext/exerr"
)
type WHandlerFunc func(PreContext) HTTPResponse
func Wrap(w *GinWrapper, fn WHandlerFunc) gin.HandlerFunc {
return func(g *gin.Context) {
reqctx := g.Request.Context()
wrap, stackTrace, panicObj := callPanicSafe(fn, PreContext{wrapper: w, ginCtx: g})
if panicObj != nil {
fmt.Printf("\n======== ======== STACKTRACE ======== ========\n%s\n======== ======== ======== ========\n\n", stackTrace)
err := exerr.
New(exerr.TypePanic, "Panic occured (in gin handler)").
Any("panicObj", panicObj).
Str("trace", stackTrace).
Build()
wrap = Error(err)
}
if g.Writer.Written() {
panic("Writing in WrapperFunc is not supported")
}
if reqctx.Err() == nil {
wrap.Write(g)
}
}
}

130
ginext/preContext.go Normal file
View File

@@ -0,0 +1,130 @@
package ginext
import (
"context"
"fmt"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"gogs.mikescher.com/BlackForestBytes/goext/exerr"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"runtime/debug"
)
type PreContext struct {
ginCtx *gin.Context
wrapper *GinWrapper
uri any
query any
body any
form any
header any
}
func (pctx *PreContext) URI(uri any) *PreContext {
pctx.uri = uri
return pctx
}
func (pctx *PreContext) Query(query any) *PreContext {
pctx.query = query
return pctx
}
func (pctx *PreContext) Body(body any) *PreContext {
pctx.body = body
return pctx
}
func (pctx *PreContext) Form(form any) *PreContext {
pctx.form = form
return pctx
}
func (pctx *PreContext) Header(header any) *PreContext {
pctx.header = header
return pctx
}
func (pctx PreContext) Start() (*AppContext, *gin.Context, *HTTPResponse) {
if pctx.uri != nil {
if err := pctx.ginCtx.ShouldBindUri(pctx.uri); err != nil {
err = exerr.Wrap(err, "Failed to read uri").
WithType(exerr.TypeBindFailURI).
Str("struct_type", fmt.Sprintf("%T", pctx.uri)).
Build()
return nil, nil, langext.Ptr(Error(err))
}
}
if pctx.query != nil {
if err := pctx.ginCtx.ShouldBindQuery(pctx.query); err != nil {
err = exerr.Wrap(err, "Failed to read query").
WithType(exerr.TypeBindFailQuery).
Str("struct_type", fmt.Sprintf("%T", pctx.query)).
Build()
return nil, nil, langext.Ptr(Error(err))
}
}
if pctx.body != nil {
if pctx.ginCtx.ContentType() == "application/json" {
if err := pctx.ginCtx.ShouldBindJSON(pctx.body); err != nil {
err = exerr.Wrap(err, "Failed to read json-body").
WithType(exerr.TypeBindFailJSON).
Str("struct_type", fmt.Sprintf("%T", pctx.body)).
Build()
return nil, nil, langext.Ptr(Error(err))
}
} else {
err := exerr.New(exerr.TypeBindFailJSON, "missing JSON body").
Str("struct_type", fmt.Sprintf("%T", pctx.body)).
Build()
return nil, nil, langext.Ptr(Error(err))
}
}
if pctx.form != nil {
if pctx.ginCtx.ContentType() == "multipart/form-data" {
if err := pctx.ginCtx.ShouldBindWith(pctx.form, binding.Form); err != nil {
err = exerr.Wrap(err, "Failed to read multipart-form").
WithType(exerr.TypeBindFailFormData).
Str("struct_type", fmt.Sprintf("%T", pctx.form)).
Build()
return nil, nil, langext.Ptr(Error(err))
}
} else {
err := exerr.New(exerr.TypeBindFailFormData, "missing form body").
Str("struct_type", fmt.Sprintf("%T", pctx.form)).
Build()
return nil, nil, langext.Ptr(Error(err))
}
}
if pctx.header != nil {
if err := pctx.ginCtx.ShouldBindHeader(pctx.query); err != nil {
err = exerr.Wrap(err, "Failed to read header").
WithType(exerr.TypeBindFailHeader).
Str("struct_type", fmt.Sprintf("%T", pctx.query)).
Build()
return nil, nil, langext.Ptr(Error(err))
}
}
ictx, cancel := context.WithTimeout(context.Background(), pctx.wrapper.requestTimeout)
actx := CreateAppContext(pctx.ginCtx, ictx, cancel)
return actx, pctx.ginCtx, nil
}
func callPanicSafe(fn WHandlerFunc, pctx PreContext) (res HTTPResponse, stackTrace string, panicObj any) {
defer func() {
if rec := recover(); rec != nil {
res = nil
stackTrace = string(debug.Stack())
panicObj = rec
}
}()
res = fn(pctx)
return res, "", nil
}

216
ginext/response.go Normal file
View File

@@ -0,0 +1,216 @@
package ginext
import (
"fmt"
"github.com/gin-gonic/gin"
"gogs.mikescher.com/BlackForestBytes/goext/exerr"
json "gogs.mikescher.com/BlackForestBytes/goext/gojson"
)
type headerval struct {
Key string
Val string
}
type HTTPResponse interface {
Write(g *gin.Context)
WithHeader(k string, v string) HTTPResponse
}
type jsonHTTPResponse struct {
statusCode int
data any
headers []headerval
}
func (j jsonHTTPResponse) Write(g *gin.Context) {
for _, v := range j.headers {
g.Header(v.Key, v.Val)
}
g.Render(j.statusCode, json.GoJsonRender{Data: j.data, NilSafeSlices: true, NilSafeMaps: true})
}
func (j jsonHTTPResponse) WithHeader(k string, v string) HTTPResponse {
j.headers = append(j.headers, headerval{k, v})
return j
}
type emptyHTTPResponse struct {
statusCode int
headers []headerval
}
func (j emptyHTTPResponse) Write(g *gin.Context) {
for _, v := range j.headers {
g.Header(v.Key, v.Val)
}
g.Status(j.statusCode)
}
func (j emptyHTTPResponse) WithHeader(k string, v string) HTTPResponse {
j.headers = append(j.headers, headerval{k, v})
return j
}
type textHTTPResponse struct {
statusCode int
data string
headers []headerval
}
func (j textHTTPResponse) Write(g *gin.Context) {
for _, v := range j.headers {
g.Header(v.Key, v.Val)
}
g.String(j.statusCode, "%s", j.data)
}
func (j textHTTPResponse) WithHeader(k string, v string) HTTPResponse {
j.headers = append(j.headers, headerval{k, v})
return j
}
type dataHTTPResponse struct {
statusCode int
data []byte
contentType string
headers []headerval
}
func (j dataHTTPResponse) Write(g *gin.Context) {
for _, v := range j.headers {
g.Header(v.Key, v.Val)
}
g.Data(j.statusCode, j.contentType, j.data)
}
func (j dataHTTPResponse) WithHeader(k string, v string) HTTPResponse {
j.headers = append(j.headers, headerval{k, v})
return j
}
type fileHTTPResponse struct {
mimetype string
filepath string
filename *string
headers []headerval
}
func (j fileHTTPResponse) Write(g *gin.Context) {
g.Header("Content-Type", j.mimetype) // if we don't set it here gin does weird file-sniffing later...
if j.filename != nil {
g.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", *j.filename))
}
for _, v := range j.headers {
g.Header(v.Key, v.Val)
}
g.File(j.filepath)
}
func (j fileHTTPResponse) WithHeader(k string, v string) HTTPResponse {
j.headers = append(j.headers, headerval{k, v})
return j
}
type downloadDataHTTPResponse struct {
statusCode int
mimetype string
data []byte
filename *string
headers []headerval
}
func (j downloadDataHTTPResponse) Write(g *gin.Context) {
g.Header("Content-Type", j.mimetype) // if we don't set it here gin does weird file-sniffing later...
if j.filename != nil {
g.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", *j.filename))
}
for _, v := range j.headers {
g.Header(v.Key, v.Val)
}
g.Data(j.statusCode, j.mimetype, j.data)
}
func (j downloadDataHTTPResponse) WithHeader(k string, v string) HTTPResponse {
j.headers = append(j.headers, headerval{k, v})
return j
}
type redirectHTTPResponse struct {
statusCode int
url string
headers []headerval
}
func (j redirectHTTPResponse) Write(g *gin.Context) {
g.Redirect(j.statusCode, j.url)
}
func (j redirectHTTPResponse) WithHeader(k string, v string) HTTPResponse {
j.headers = append(j.headers, headerval{k, v})
return j
}
type jsonAPIErrResponse struct {
err *exerr.ExErr
headers []headerval
}
func (j jsonAPIErrResponse) Write(g *gin.Context) {
j.err.Output(g)
}
func (j jsonAPIErrResponse) WithHeader(k string, v string) HTTPResponse {
j.headers = append(j.headers, headerval{k, v})
return j
}
func Status(sc int) HTTPResponse {
return &emptyHTTPResponse{statusCode: sc}
}
func JSON(sc int, data any) HTTPResponse {
return &jsonHTTPResponse{statusCode: sc, data: data}
}
func Data(sc int, contentType string, data []byte) HTTPResponse {
return &dataHTTPResponse{statusCode: sc, contentType: contentType, data: data}
}
func Text(sc int, data string) HTTPResponse {
return &textHTTPResponse{statusCode: sc, data: data}
}
func File(mimetype string, filepath string) HTTPResponse {
return &fileHTTPResponse{mimetype: mimetype, filepath: filepath}
}
func Download(mimetype string, filepath string, filename string) HTTPResponse {
return &fileHTTPResponse{mimetype: mimetype, filepath: filepath, filename: &filename}
}
func DownloadData(mimetype string, filename string, data []byte) HTTPResponse {
return &downloadDataHTTPResponse{mimetype: mimetype, data: data, filename: &filename}
}
func Redirect(sc int, newURL string) HTTPResponse {
return &redirectHTTPResponse{statusCode: sc, url: newURL}
}
func Error(e error) HTTPResponse {
return &jsonAPIErrResponse{
err: exerr.FromError(e),
}
}
func ErrWrap(e error, errorType exerr.ErrorType, msg string) HTTPResponse {
return &jsonAPIErrResponse{
err: exerr.FromError(exerr.Wrap(e, msg).WithType(errorType).Build()),
}
}
func NotImplemented() HTTPResponse {
return Error(exerr.New(exerr.TypeNotImplemented, "").Build())
}

199
ginext/routes.go Normal file
View File

@@ -0,0 +1,199 @@
package ginext
import (
"github.com/gin-gonic/gin"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/rext"
"net/http"
"path"
"reflect"
"regexp"
"runtime"
"strings"
)
var anyMethods = []string{
http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch,
http.MethodHead, http.MethodOptions, http.MethodDelete, http.MethodConnect,
http.MethodTrace,
}
type GinRoutesWrapper struct {
wrapper *GinWrapper
routes gin.IRouter
absPath string
defaultHandler []gin.HandlerFunc
}
type GinRouteBuilder struct {
routes *GinRoutesWrapper
method string
relPath string
absPath string
handlers []gin.HandlerFunc
}
func (w *GinWrapper) Routes() *GinRoutesWrapper {
return &GinRoutesWrapper{
wrapper: w,
routes: w.engine,
absPath: "",
defaultHandler: make([]gin.HandlerFunc, 0),
}
}
func (w *GinRoutesWrapper) Group(relativePath string) *GinRoutesWrapper {
return &GinRoutesWrapper{
wrapper: w.wrapper,
routes: w.routes.Group(relativePath),
defaultHandler: langext.ArrCopy(w.defaultHandler),
absPath: joinPaths(w.absPath, relativePath),
}
}
func (w *GinRoutesWrapper) Use(middleware ...gin.HandlerFunc) *GinRoutesWrapper {
defHandler := langext.ArrCopy(w.defaultHandler)
defHandler = append(defHandler, middleware...)
return &GinRoutesWrapper{wrapper: w.wrapper, routes: w.routes, defaultHandler: defHandler}
}
func (w *GinRoutesWrapper) GET(relativePath string) *GinRouteBuilder {
return w._route(http.MethodGet, relativePath)
}
func (w *GinRoutesWrapper) POST(relativePath string) *GinRouteBuilder {
return w._route(http.MethodPost, relativePath)
}
func (w *GinRoutesWrapper) DELETE(relativePath string) *GinRouteBuilder {
return w._route(http.MethodDelete, relativePath)
}
func (w *GinRoutesWrapper) PATCH(relativePath string) *GinRouteBuilder {
return w._route(http.MethodPatch, relativePath)
}
func (w *GinRoutesWrapper) PUT(relativePath string) *GinRouteBuilder {
return w._route(http.MethodPut, relativePath)
}
func (w *GinRoutesWrapper) OPTIONS(relativePath string) *GinRouteBuilder {
return w._route(http.MethodOptions, relativePath)
}
func (w *GinRoutesWrapper) HEAD(relativePath string) *GinRouteBuilder {
return w._route(http.MethodHead, relativePath)
}
func (w *GinRoutesWrapper) COUNT(relativePath string) *GinRouteBuilder {
return w._route("COUNT", relativePath)
}
func (w *GinRoutesWrapper) Any(relativePath string) *GinRouteBuilder {
return w._route("*", relativePath)
}
func (w *GinRoutesWrapper) _route(method string, relativePath string) *GinRouteBuilder {
return &GinRouteBuilder{
routes: w,
method: method,
relPath: relativePath,
absPath: joinPaths(w.absPath, relativePath),
handlers: langext.ArrCopy(w.defaultHandler),
}
}
func (w *GinRouteBuilder) Use(middleware ...gin.HandlerFunc) *GinRouteBuilder {
w.handlers = append(w.handlers, middleware...)
return w
}
func (w *GinRouteBuilder) Handle(handler WHandlerFunc) {
if w.routes.wrapper.bufferBody {
arr := make([]gin.HandlerFunc, 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) })
handlerName := nameOfFunction(handler)
w.handlers = append(w.handlers, Wrap(w.routes.wrapper, handler))
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: handlerName,
})
}
func (w *GinWrapper) NoRoute(handler WHandlerFunc) {
w.engine.NoRoute(Wrap(w, handler))
w.routeSpecs = append(w.routeSpecs, ginRouteSpec{
Method: "ANY",
URL: "[NO_ROUTE]",
Middlewares: nil,
Handler: nameOfFunction(handler),
})
}
func nameOfFunction(f any) string {
fname := runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name()
split := strings.Split(fname, "/")
if len(split) == 0 {
return ""
}
fname = split[len(split)-1]
// https://stackoverflow.com/a/32925345/1761622
if strings.HasSuffix(fname, "-fm") {
fname = fname[:len(fname)-len("-fm")]
}
suffix := rext.W(regexp.MustCompile(`\.func[0-9]+(?:\.[0-9]+)*$`))
if match, ok := suffix.MatchFirst(fname); ok {
fname = fname[:len(fname)-match.FullMatch().Length()]
}
return fname
}
// joinPaths is copied verbatim from gin@v1.9.1/gin.go
func joinPaths(absolutePath, relativePath string) string {
if relativePath == "" {
return absolutePath
}
finalPath := path.Join(absolutePath, relativePath)
if lastChar(relativePath) == '/' && lastChar(finalPath) != '/' {
return finalPath + "/"
}
return finalPath
}
func lastChar(str string) uint8 {
if str == "" {
panic("The length of the string can't be 0")
}
return str[len(str)-1]
}

40
go.mod
View File

@@ -3,22 +3,48 @@ module gogs.mikescher.com/BlackForestBytes/goext
go 1.19 go 1.19
require ( require (
github.com/gin-gonic/gin v1.9.1
github.com/jmoiron/sqlx v1.3.5 github.com/jmoiron/sqlx v1.3.5
go.mongodb.org/mongo-driver v1.11.7 github.com/rs/xid v1.5.0
golang.org/x/crypto v0.9.0 github.com/rs/zerolog v1.30.0
golang.org/x/sys v0.8.0 go.mongodb.org/mongo-driver v1.12.1
golang.org/x/term v0.8.0 golang.org/x/crypto v0.12.0
golang.org/x/sys v0.11.0
golang.org/x/term v0.11.0
) )
require ( require (
github.com/bytedance/sonic v1.10.0-rc3 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
github.com/chenzhuoyu/iasm v0.9.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.14.1 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/golang/snappy v0.0.4 // indirect github.com/golang/snappy v0.0.4 // indirect
github.com/klauspost/compress v1.16.5 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.16.7 // indirect
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/montanaflynn/stats v0.7.1 // indirect github.com/montanaflynn/stats v0.7.1 // indirect
github.com/pelletier/go-toml/v2 v2.0.9 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect github.com/xdg-go/scram v1.1.2 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect
golang.org/x/sync v0.2.0 // indirect golang.org/x/arch v0.4.0 // indirect
golang.org/x/text v0.9.0 // indirect golang.org/x/net v0.14.0 // indirect
golang.org/x/sync v0.3.0 // indirect
golang.org/x/text v0.12.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
) )

156
go.sum
View File

@@ -1,42 +1,110 @@
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
github.com/bytedance/sonic v1.10.0-rc2 h1:oDfRZ+4m6AYCOC0GFeOCeYqvBmucy1isvouS2K0cPzo=
github.com/bytedance/sonic v1.10.0-rc2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
github.com/bytedance/sonic v1.10.0-rc3 h1:uNSnscRapXTwUgTyOF0GVljYD08p9X/Lbr9MweSV3V0=
github.com/bytedance/sonic v1.10.0-rc3/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=
github.com/chenzhuoyu/iasm v0.9.0 h1:9fhXjVzq5hUy2gkhhgHl95zG2cEAhw9OSGs8toWWAwo=
github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.14.1 h1:9c50NUPC30zyuKprjL3vNZ0m5oG+jU0zvx4AqHGnv4k=
github.com/go-playground/validator/v10 v10.14.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI= github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0=
github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w=
github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
github.com/rs/zerolog v1.29.1 h1:cO+d60CHkknCbvzEWxP0S9K6KqyTjrCNUy1LdQLCGPc=
github.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU=
github.com/rs/zerolog v1.30.0 h1:SymVODrcRsaRaSInD9yQtKbtWqwsfoPcRff/oRXLj4c=
github.com/rs/zerolog v1.30.0/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiSi1w=
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/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.1 h1:VOMT+81stJgXW3CpHyqHN3AXDYIMsx56mEFrB37Mb/E= github.com/xdg-go/scram v1.1.1 h1:VOMT+81stJgXW3CpHyqHN3AXDYIMsx56mEFrB37Mb/E=
@@ -52,47 +120,74 @@ github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7Jul
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqToslyjUt3VOPF4J7aK/3MPcK7xp3PDk= github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqToslyjUt3VOPF4J7aK/3MPcK7xp3PDk=
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/R083muKhosV54bj5niojjWZvU8xrevuH4= github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/R083muKhosV54bj5niojjWZvU8xrevuH4=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.mongodb.org/mongo-driver v1.11.1 h1:QP0znIRTuL0jf1oBQoAoM0C6ZJfBK4kx0Uumtv1A7w8= go.mongodb.org/mongo-driver v1.11.2 h1:+1v2rDQUWNcGW7/7E0Jvdz51V38XXxJfhzbV17aNHCw=
go.mongodb.org/mongo-driver v1.11.1/go.mod h1:s7p5vEtfbeR1gYi6pnj3c3/urpbLv2T5Sfd6Rp2HBB8= go.mongodb.org/mongo-driver v1.11.2/go.mod h1:s7p5vEtfbeR1gYi6pnj3c3/urpbLv2T5Sfd6Rp2HBB8=
go.mongodb.org/mongo-driver v1.11.7 h1:LIwYxASDLGUg/8wOhgOOZhX8tQa/9tgZPgzZoVqJvcs= go.mongodb.org/mongo-driver v1.12.0 h1:aPx33jmn/rQuJXPQLZQ8NtfPQG8CaqgLThFtqRb0PiE=
go.mongodb.org/mongo-driver v1.11.7/go.mod h1:G9TgswdsWjX4tmDA5zfs2+6AEPpYJwqblyjsfuh8oXY= go.mongodb.org/mongo-driver v1.12.0/go.mod h1:AZkxhPnFJUoH7kZlFkVKucV20K387miPfm7oimrSmK0=
go.mongodb.org/mongo-driver v1.12.1 h1:nLkghSU8fQNaK7oUmDhQFsnrtcoNy7Z6LVFKsEecqgE=
go.mongodb.org/mongo-driver v1.12.1/go.mod h1:/rGBTebI3XYboVmgz+Wv3Bcbl3aD0QF9zl6kDDw18rQ=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.4.0 h1:A8WCeEWhLwPBKNbFi5Wv5UTCBx5zzubnXDlMOFAzFMc=
golang.org/x/arch v0.4.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8= golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8=
golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80= golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80=
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
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/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-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-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
golang.org/x/net v0.13.0 h1:Nvo8UFsZ8X3BhAC9699Z1j7XQ3rsZnUUm7jfBEk1ueY=
golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
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-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI= golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-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.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.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.1.0 h1:g6Z6vPFA9dYBAF7DWcH6sCcOntplXsDKcliusYijMlw=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI= golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI=
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols= golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0=
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
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.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -100,16 +195,23 @@ 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.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.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/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=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View File

@@ -1,5 +1,5 @@
package goext package goext
const GoextVersion = "0.0.153" const GoextVersion = "0.0.221"
const GoextVersionTimestamp = "2023-06-08T16:24:15+0200" const GoextVersionTimestamp = "2023-08-06T19:10:31+0200"

View File

@@ -440,3 +440,30 @@ func ArrCopy[T any](in []T) []T {
copy(out, in) copy(out, in)
return out return out
} }
func ArrRemove[T comparable](arr []T, needle T) []T {
idx := ArrFirstIndex(arr, needle)
if idx >= 0 {
return append(arr[:idx], arr[idx+1:]...)
}
return arr
}
func ArrExcept[T comparable](arr []T, needles ...T) []T {
r := make([]T, 0, len(arr))
rmlist := ArrToSet(needles)
for _, v := range arr {
if _, ok := rmlist[v]; !ok {
r = append(r, v)
}
}
return r
}
func ArrayToInterface[T any](t []T) []interface{} {
res := make([]interface{}, 0, len(t))
for i, _ := range t {
res = append(res, t[i])
}
return res
}

View File

@@ -29,6 +29,14 @@ func ArrToMap[T comparable, V any](a []V, keyfunc func(V) T) map[T]V {
return result return result
} }
func ArrToSet[T comparable](a []T) map[T]bool {
result := make(map[T]bool, len(a))
for _, v := range a {
result[v] = true
}
return result
}
func MapToArr[T comparable, V any](v map[T]V) []MapEntry[T, V] { func MapToArr[T comparable, V any](v map[T]V) []MapEntry[T, V] {
result := make([]MapEntry[T, V], 0, len(v)) result := make([]MapEntry[T, V], 0, len(v))
for mk, mv := range v { for mk, mv := range v {

View File

@@ -16,6 +16,9 @@ func CreateGoExtBsonRegistry() *bsoncodec.Registry {
rb.RegisterTypeDecoder(reflect.TypeOf(rfctime.RFC3339NanoTime{}), rfctime.RFC3339NanoTime{}) rb.RegisterTypeDecoder(reflect.TypeOf(rfctime.RFC3339NanoTime{}), rfctime.RFC3339NanoTime{})
rb.RegisterTypeDecoder(reflect.TypeOf(&rfctime.RFC3339NanoTime{}), rfctime.RFC3339NanoTime{}) rb.RegisterTypeDecoder(reflect.TypeOf(&rfctime.RFC3339NanoTime{}), rfctime.RFC3339NanoTime{})
rb.RegisterTypeDecoder(reflect.TypeOf(rfctime.Date{}), rfctime.Date{})
rb.RegisterTypeDecoder(reflect.TypeOf(&rfctime.Date{}), rfctime.Date{})
bsoncodec.DefaultValueEncoders{}.RegisterDefaultEncoders(rb) bsoncodec.DefaultValueEncoders{}.RegisterDefaultEncoders(rb)
bsoncodec.DefaultValueDecoders{}.RegisterDefaultDecoders(rb) bsoncodec.DefaultValueDecoders{}.RegisterDefaultDecoders(rb)

View File

@@ -8,7 +8,7 @@ import (
func TestGroupByNameOrEmpty1(t *testing.T) { func TestGroupByNameOrEmpty1(t *testing.T) {
regex1 := W(regexp.MustCompile("0(?P<group1>A+)B(?P<group2>C+)0")) regex1 := W(regexp.MustCompile(`0(?P<group1>A+)B(?P<group2>C+)0`))
match1, ok1 := regex1.MatchFirst("Hello 0AAAABCCC0 Bye.") match1, ok1 := regex1.MatchFirst("Hello 0AAAABCCC0 Bye.")
@@ -26,7 +26,7 @@ func TestGroupByNameOrEmpty1(t *testing.T) {
func TestGroupByNameOrEmpty2(t *testing.T) { func TestGroupByNameOrEmpty2(t *testing.T) {
regex1 := W(regexp.MustCompile("0(?P<group1>A+)B(?P<group2>C+)(?P<group3>C+)?0")) regex1 := W(regexp.MustCompile(`0(?P<group1>A+)B(?P<group2>C+)(?P<group3>C+)?0`))
match1, ok1 := regex1.MatchFirst("Hello 0AAAABCCC0 Bye.") match1, ok1 := regex1.MatchFirst("Hello 0AAAABCCC0 Bye.")

240
rfctime/date.go Normal file
View File

@@ -0,0 +1,240 @@
package rfctime
import (
"encoding/json"
"errors"
"fmt"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/bsoncodec"
"go.mongodb.org/mongo-driver/bson/bsonrw"
"go.mongodb.org/mongo-driver/bson/bsontype"
"reflect"
"time"
)
type Date struct {
Year int
Month int
Day int
}
func (t Date) Time(loc *time.Location) time.Time {
return time.Date(t.Year, time.Month(t.Month), t.Day, 0, 0, 0, 0, loc)
}
func (t Date) TimeUTC() time.Time {
return time.Date(t.Year, time.Month(t.Month), t.Day, 0, 0, 0, 0, time.UTC)
}
func (t Date) TimeLocal() time.Time {
return time.Date(t.Year, time.Month(t.Month), t.Day, 0, 0, 0, 0, time.Local)
}
func (t Date) MarshalBinary() ([]byte, error) {
return t.TimeUTC().MarshalBinary()
}
func (t *Date) UnmarshalBinary(data []byte) error {
nt := time.Time{}
if err := nt.UnmarshalBinary(data); err != nil {
return err
}
t.Year = nt.Year()
t.Month = int(nt.Month())
t.Day = nt.Day()
return nil
}
func (t Date) GobEncode() ([]byte, error) {
return t.TimeUTC().GobEncode()
}
func (t *Date) GobDecode(data []byte) error {
nt := time.Time{}
if err := nt.GobDecode(data); err != nil {
return err
}
t.Year = nt.Year()
t.Month = int(nt.Month())
t.Day = nt.Day()
return nil
}
func (t *Date) UnmarshalJSON(data []byte) error {
str := ""
if err := json.Unmarshal(data, &str); err != nil {
return err
}
t0, err := time.Parse(t.FormatStr(), str)
if err != nil {
return err
}
t.Year = t0.Year()
t.Month = int(t0.Month())
t.Day = t0.Day()
return nil
}
func (t Date) MarshalJSON() ([]byte, error) {
str := t.TimeUTC().Format(t.FormatStr())
return json.Marshal(str)
}
func (t Date) MarshalText() ([]byte, error) {
b := make([]byte, 0, len(t.FormatStr()))
return t.TimeUTC().AppendFormat(b, t.FormatStr()), nil
}
func (t *Date) UnmarshalText(data []byte) error {
var err error
v, err := time.Parse(t.FormatStr(), string(data))
if err != nil {
return err
}
t.Year = v.Year()
t.Month = int(v.Month())
t.Day = v.Day()
return nil
}
func (t *Date) UnmarshalBSONValue(bt bsontype.Type, data []byte) error {
if bt == bsontype.Null {
// we can't set nil in UnmarshalBSONValue (so we use default(struct))
// Use mongoext.CreateGoExtBsonRegistry if you need to unmarsh pointer values
// https://stackoverflow.com/questions/75167597
// https://jira.mongodb.org/browse/GODRIVER-2252
*t = Date{}
return nil
}
if bt != bsontype.String {
return errors.New(fmt.Sprintf("cannot unmarshal %v into Date", bt))
}
var tt string
err := bson.RawValue{Type: bt, Value: data}.Unmarshal(&tt)
if err != nil {
return err
}
v, err := time.Parse(t.FormatStr(), tt)
if err != nil {
return err
}
t.Year = v.Year()
t.Month = int(v.Month())
t.Day = v.Day()
return nil
}
func (t Date) MarshalBSONValue() (bsontype.Type, []byte, error) {
return bson.MarshalValue(t.TimeUTC().Format(t.FormatStr()))
}
func (t Date) DecodeValue(dc bsoncodec.DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error {
if val.Kind() == reflect.Ptr && val.IsNil() {
if !val.CanSet() {
return errors.New("ValueUnmarshalerDecodeValue")
}
val.Set(reflect.New(val.Type().Elem()))
}
tp, src, err := bsonrw.Copier{}.CopyValueToBytes(vr)
if err != nil {
return err
}
if val.Kind() == reflect.Ptr && len(src) == 0 {
val.Set(reflect.Zero(val.Type()))
return nil
}
err = t.UnmarshalBSONValue(tp, src)
if err != nil {
return err
}
if val.Kind() == reflect.Ptr {
val.Set(reflect.ValueOf(&t))
} else {
val.Set(reflect.ValueOf(t))
}
return nil
}
func (t Date) Serialize() string {
return t.TimeUTC().Format(t.FormatStr())
}
func (t Date) FormatStr() string {
return "2006-01-02"
}
func (t Date) Date() (year int, month time.Month, day int) {
return t.TimeUTC().Date()
}
func (t Date) Weekday() time.Weekday {
return t.TimeUTC().Weekday()
}
func (t Date) ISOWeek() (year, week int) {
return t.TimeUTC().ISOWeek()
}
func (t Date) YearDay() int {
return t.TimeUTC().YearDay()
}
func (t Date) AddDate(years int, months int, days int) Date {
return NewDate(t.TimeUTC().AddDate(years, months, days))
}
func (t Date) Unix() int64 {
return t.TimeUTC().Unix()
}
func (t Date) UnixMilli() int64 {
return t.TimeUTC().UnixMilli()
}
func (t Date) UnixMicro() int64 {
return t.TimeUTC().UnixMicro()
}
func (t Date) UnixNano() int64 {
return t.TimeUTC().UnixNano()
}
func (t Date) Format(layout string) string {
return t.TimeUTC().Format(layout)
}
func (t Date) GoString() string {
return t.TimeUTC().GoString()
}
func (t Date) String() string {
return t.TimeUTC().String()
}
func NewDate(t time.Time) Date {
return Date{
Year: t.Year(),
Month: int(t.Month()),
Day: t.Day(),
}
}
func NowDate(loc *time.Location) Date {
return NewDate(time.Now().In(loc))
}
func NowDateLoc() Date {
return NewDate(time.Now().In(time.UTC))
}
func NowDateUTC() Date {
return NewDate(time.Now().In(time.Local))
}

View File

@@ -39,7 +39,7 @@ func HashSqliteSchema(ctx context.Context, schemaStr string) (string, error) {
return HashSqliteDatabase(ctx, db) return HashSqliteDatabase(ctx, db)
} }
func HashSqliteDatabase(ctx context.Context, db DB) (string, error) { func HashSqliteDatabase(ctx context.Context, db Queryable) (string, error) {
ss, err := CreateSqliteDatabaseSchemaString(ctx, db) ss, err := CreateSqliteDatabaseSchemaString(ctx, db)
if err != nil { if err != nil {
return "", err return "", err
@@ -50,7 +50,7 @@ func HashSqliteDatabase(ctx context.Context, db DB) (string, error) {
return hex.EncodeToString(cs[:]), nil return hex.EncodeToString(cs[:]), nil
} }
func CreateSqliteDatabaseSchemaString(ctx context.Context, db DB) (string, error) { func CreateSqliteDatabaseSchemaString(ctx context.Context, db Queryable) (string, error) {
type colInfo struct { type colInfo struct {
Name string `db:"name"` Name string `db:"name"`

View File

@@ -7,24 +7,40 @@ import (
"gogs.mikescher.com/BlackForestBytes/goext/langext" "gogs.mikescher.com/BlackForestBytes/goext/langext"
) )
type TxStatus string
const (
TxStatusInitial TxStatus = "INITIAL"
TxStatusActive TxStatus = "ACTIVE"
TxStatusComitted TxStatus = "COMMITTED"
TxStatusRollback TxStatus = "ROLLBACK"
)
type Tx interface { type Tx interface {
Rollback() error Rollback() error
Commit() error Commit() error
Status() TxStatus
Exec(ctx context.Context, sql string, prep PP) (sql.Result, error) Exec(ctx context.Context, sql string, prep PP) (sql.Result, error)
Query(ctx context.Context, sql string, prep PP) (*sqlx.Rows, error) Query(ctx context.Context, sql string, prep PP) (*sqlx.Rows, error)
} }
type transaction struct { type transaction struct {
tx *sqlx.Tx tx *sqlx.Tx
id uint16 id uint16
lstr []Listener lstr []Listener
status TxStatus
execCtr int
queryCtr int
} }
func NewTransaction(xtx *sqlx.Tx, txid uint16, lstr []Listener) Tx { func NewTransaction(xtx *sqlx.Tx, txid uint16, lstr []Listener) Tx {
return &transaction{ return &transaction{
tx: xtx, tx: xtx,
id: txid, id: txid,
lstr: lstr, lstr: lstr,
status: TxStatusInitial,
execCtr: 0,
queryCtr: 0,
} }
} }
@@ -38,6 +54,10 @@ func (tx *transaction) Rollback() error {
result := tx.tx.Rollback() result := tx.tx.Rollback()
if result == nil {
tx.status = TxStatusRollback
}
for _, v := range tx.lstr { for _, v := range tx.lstr {
v.PostTxRollback(tx.id, result) v.PostTxRollback(tx.id, result)
} }
@@ -55,6 +75,10 @@ func (tx *transaction) Commit() error {
result := tx.tx.Commit() result := tx.tx.Commit()
if result == nil {
tx.status = TxStatusComitted
}
for _, v := range tx.lstr { for _, v := range tx.lstr {
v.PostTxRollback(tx.id, result) v.PostTxRollback(tx.id, result)
} }
@@ -73,6 +97,10 @@ func (tx *transaction) Exec(ctx context.Context, sqlstr string, prep PP) (sql.Re
res, err := tx.tx.NamedExecContext(ctx, sqlstr, prep) res, err := tx.tx.NamedExecContext(ctx, sqlstr, prep)
if tx.status == TxStatusInitial && err == nil {
tx.status = TxStatusActive
}
for _, v := range tx.lstr { for _, v := range tx.lstr {
v.PostExec(langext.Ptr(tx.id), origsql, sqlstr, prep) v.PostExec(langext.Ptr(tx.id), origsql, sqlstr, prep)
} }
@@ -94,6 +122,10 @@ func (tx *transaction) Query(ctx context.Context, sqlstr string, prep PP) (*sqlx
rows, err := sqlx.NamedQueryContext(ctx, tx.tx, sqlstr, prep) rows, err := sqlx.NamedQueryContext(ctx, tx.tx, sqlstr, prep)
if tx.status == TxStatusInitial && err == nil {
tx.status = TxStatusActive
}
for _, v := range tx.lstr { for _, v := range tx.lstr {
v.PostQuery(langext.Ptr(tx.id), origsql, sqlstr, prep) v.PostQuery(langext.Ptr(tx.id), origsql, sqlstr, prep)
} }
@@ -103,3 +135,11 @@ func (tx *transaction) Query(ctx context.Context, sqlstr string, prep PP) (*sqlx
} }
return rows, nil return rows, nil
} }
func (tx *transaction) Status() TxStatus {
return tx.status
}
func (tx *transaction) Traffic() (int, int) {
return tx.execCtr, tx.queryCtr
}

View File

@@ -71,12 +71,12 @@ func SupportsColors() bool {
} }
} }
var term256Regex = regexp.MustCompile("(?i)-256(color)?$") var term256Regex = regexp.MustCompile(`(?i)-256(color)?$`)
if term256Regex.MatchString(termenv) { if term256Regex.MatchString(termenv) {
return true return true
} }
var termBasicRegex = regexp.MustCompile("(?i)^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux") var termBasicRegex = regexp.MustCompile(`(?i)^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux`)
if termBasicRegex.MatchString(termenv) { if termBasicRegex.MatchString(termenv) {
return true return true

View File

@@ -2,6 +2,7 @@ package tst
import ( import (
"encoding/hex" "encoding/hex"
"runtime/debug"
"testing" "testing"
) )
@@ -54,12 +55,18 @@ func AssertHexEqual(t *testing.T, expected string, actual []byte) {
func AssertTrue(t *testing.T, value bool) { func AssertTrue(t *testing.T, value bool) {
if !value { if !value {
t.Error("value should be true") t.Error("value should be true\n" + string(debug.Stack()))
} }
} }
func AssertFalse(t *testing.T, value bool) { func AssertFalse(t *testing.T, value bool) {
if value { if value {
t.Error("value should be false") t.Error("value should be false\n" + string(debug.Stack()))
}
}
func AssertNoErr(t *testing.T, anerr error) {
if anerr != nil {
t.Error("Function returned an error: " + anerr.Error() + "\n" + string(debug.Stack()))
} }
} }

View File

@@ -2,15 +2,32 @@ package wmo
import ( import (
"context" "context"
"go.mongodb.org/mongo-driver/bson/bsontype"
"go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo"
ct "gogs.mikescher.com/BlackForestBytes/goext/cursortoken" ct "gogs.mikescher.com/BlackForestBytes/goext/cursortoken"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "gogs.mikescher.com/BlackForestBytes/goext/langext"
"reflect" "reflect"
) )
type EntityID = any type EntityID interface {
MarshalBSONValue() (bsontype.Type, []byte, error)
String() string
}
type fullTypeRef[TData any] struct { type Decodable interface {
Decode(v any) error
}
type Cursorable interface {
Decode(v any) error
Err() error
Close(ctx context.Context) error
All(ctx context.Context, results any) error
RemainingBatchLength() int
Next(ctx context.Context) bool
}
type fullTypeRef struct {
IsPointer bool IsPointer bool
Kind reflect.Kind Kind reflect.Kind
RealType reflect.Type RealType reflect.Type
@@ -21,8 +38,11 @@ type fullTypeRef[TData any] struct {
} }
type Coll[TData any] struct { type Coll[TData any] struct {
coll *mongo.Collection coll *mongo.Collection // internal mongo collection, access via Collection()
dataTypeMap map[string]fullTypeRef[TData] dataTypeMap map[string]fullTypeRef // list of TData fields (only if TData is not an interface)
implDataTypeMap map[reflect.Type]map[string]fullTypeRef // dynamic list of fields of TData implementations (only if TData is an interface)
customDecoder *func(ctx context.Context, dec Decodable) (TData, error) // custom decoding function (useful if TData is an interface)
isInterfaceDataType bool // true if TData is an interface (not a struct)
} }
func (c *Coll[TData]) Collection() *mongo.Collection { func (c *Coll[TData]) Collection() *mongo.Collection {
@@ -33,6 +53,14 @@ func (c *Coll[TData]) Name() string {
return c.coll.Name() return c.coll.Name()
} }
func (c *Coll[TData]) WithDecodeFunc(cdf func(ctx context.Context, dec Decodable) (TData, error), example TData) *Coll[TData] {
c.EnsureInitializedReflection(example)
c.customDecoder = langext.Ptr(cdf)
return c
}
func (c *Coll[TData]) Indexes() mongo.IndexView { func (c *Coll[TData]) Indexes() mongo.IndexView {
return c.coll.Indexes() return c.coll.Indexes()
} }

54
wmo/decoding.go Normal file
View File

@@ -0,0 +1,54 @@
package wmo
import (
"context"
)
func (c *Coll[TData]) decodeSingle(ctx context.Context, dec Decodable) (TData, error) {
if c.customDecoder != nil {
return (*c.customDecoder)(ctx, dec)
} else {
var res TData
err := dec.Decode(&res)
if err != nil {
return *new(TData), err
}
return res, nil
}
}
func (c *Coll[TData]) decodeAll(ctx context.Context, cursor Cursorable) ([]TData, error) {
if c.customDecoder != nil {
res := make([]TData, 0, cursor.RemainingBatchLength())
for cursor.Next(ctx) {
entry, err := (*c.customDecoder)(ctx, cursor)
if err != nil {
return nil, err
}
res = append(res, entry)
}
return res, nil
} else {
res := make([]TData, 0, cursor.RemainingBatchLength())
err := cursor.All(ctx, &res)
if err != nil {
return nil, err
}
return res, nil
}
}

View File

@@ -2,6 +2,7 @@ package wmo
import ( import (
"context" "context"
"errors"
"go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options" "go.mongodb.org/mongo-driver/mongo/options"
) )
@@ -12,11 +13,44 @@ func (c *Coll[TData]) Aggregate(ctx context.Context, pipeline mongo.Pipeline, op
return nil, err return nil, err
} }
res := make([]TData, 0, cursor.RemainingBatchLength()) res, err := c.decodeAll(ctx, cursor)
err = cursor.All(ctx, &res)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return res, nil return res, nil
} }
func (c *Coll[TData]) AggregateOneOpt(ctx context.Context, pipeline mongo.Pipeline, opts ...*options.AggregateOptions) (*TData, error) {
cursor, err := c.coll.Aggregate(ctx, pipeline, opts...)
if err != nil {
return nil, err
}
if cursor.Next(ctx) {
v, err := c.decodeSingle(ctx, cursor)
if err != nil {
return nil, err
}
return &v, nil
}
return nil, nil
}
func (c *Coll[TData]) AggregateOne(ctx context.Context, pipeline mongo.Pipeline, opts ...*options.AggregateOptions) (TData, error) {
cursor, err := c.coll.Aggregate(ctx, pipeline, opts...)
if err != nil {
return *new(TData), err
}
if cursor.Next(ctx) {
v, err := c.decodeSingle(ctx, cursor)
if err != nil {
return *new(TData), err
}
return v, nil
}
return *new(TData), errors.New("no document in result")
}

View File

@@ -6,7 +6,7 @@ import (
"go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo"
) )
func (c *Coll[TData]) DeleteOnebyID(ctx context.Context, id EntityID) error { func (c *Coll[TData]) DeleteOneByID(ctx context.Context, id EntityID) error {
_, err := c.coll.DeleteOne(ctx, bson.M{"_id": id}) _, err := c.coll.DeleteOne(ctx, bson.M{"_id": id})
if err != nil { if err != nil {
return err return err

View File

@@ -8,20 +8,15 @@ import (
) )
func (c *Coll[TData]) FindOne(ctx context.Context, filter bson.M) (TData, error) { func (c *Coll[TData]) FindOne(ctx context.Context, filter bson.M) (TData, error) {
var res TData mongoRes := c.coll.FindOne(ctx, filter)
err := c.coll.FindOne(ctx, filter).Decode(&res) return c.decodeSingle(ctx, mongoRes)
if err != nil {
return *new(TData), err
}
return res, nil
} }
func (c *Coll[TData]) FindOneOpt(ctx context.Context, filter bson.M) (*TData, error) { func (c *Coll[TData]) FindOneOpt(ctx context.Context, filter bson.M) (*TData, error) {
var res TData mongoRes := c.coll.FindOne(ctx, filter)
err := c.coll.FindOne(ctx, filter).Decode(&res) res, err := c.decodeSingle(ctx, mongoRes)
if err == mongo.ErrNoDocuments { if err == mongo.ErrNoDocuments {
return nil, nil return nil, nil
} }
@@ -33,20 +28,15 @@ func (c *Coll[TData]) FindOneOpt(ctx context.Context, filter bson.M) (*TData, er
} }
func (c *Coll[TData]) FindOneByID(ctx context.Context, id EntityID) (TData, error) { func (c *Coll[TData]) FindOneByID(ctx context.Context, id EntityID) (TData, error) {
var res TData mongoRes := c.coll.FindOne(ctx, bson.M{"_id": id})
err := c.coll.FindOne(ctx, bson.M{"_id": id}).Decode(&res) return c.decodeSingle(ctx, mongoRes)
if err != nil {
return *new(TData), err
}
return res, nil
} }
func (c *Coll[TData]) FindOneOptByID(ctx context.Context, id EntityID) (*TData, error) { func (c *Coll[TData]) FindOneOptByID(ctx context.Context, id EntityID) (*TData, error) {
var res TData mongoRes := c.coll.FindOne(ctx, bson.M{"_id": id})
err := c.coll.FindOne(ctx, bson.M{"_id": id}).Decode(&res) res, err := c.decodeSingle(ctx, mongoRes)
if err == mongo.ErrNoDocuments { if err == mongo.ErrNoDocuments {
return nil, nil return nil, nil
} }
@@ -63,8 +53,7 @@ func (c *Coll[TData]) Find(ctx context.Context, filter bson.M, opts ...*options.
return nil, err return nil, err
} }
res := make([]TData, 0, cursor.RemainingBatchLength()) res, err := c.decodeAll(ctx, cursor)
err = cursor.All(ctx, &res)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -3,6 +3,8 @@ package wmo
import ( import (
"context" "context"
"go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
) )
func (c *Coll[TData]) InsertOne(ctx context.Context, valueIn TData) (TData, error) { func (c *Coll[TData]) InsertOne(ctx context.Context, valueIn TData) (TData, error) {
@@ -11,12 +13,11 @@ func (c *Coll[TData]) InsertOne(ctx context.Context, valueIn TData) (TData, erro
return *new(TData), err return *new(TData), err
} }
var res TData mongoRes := c.coll.FindOne(ctx, bson.M{"_id": insRes.InsertedID})
err = c.coll.FindOne(ctx, bson.M{"_id": insRes.InsertedID}).Decode(&res) return c.decodeSingle(ctx, mongoRes)
if err != nil { }
return *new(TData), err
} func (c *Coll[TData]) InsertMany(ctx context.Context, valueIn []TData) (*mongo.InsertManyResult, error) {
return c.coll.InsertMany(ctx, langext.ArrayToInterface(valueIn))
return res, nil
} }

View File

@@ -3,6 +3,7 @@ package wmo
import ( import (
"context" "context"
"go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
ct "gogs.mikescher.com/BlackForestBytes/goext/cursortoken" ct "gogs.mikescher.com/BlackForestBytes/goext/cursortoken"
) )
@@ -11,9 +12,16 @@ func (c *Coll[TData]) List(ctx context.Context, filter ct.Filter, pageSize *int,
return make([]TData, 0), ct.End(), nil return make([]TData, 0), ct.End(), nil
} }
pipeline := filter.FilterQuery() pipeline := mongo.Pipeline{}
pf1 := "_id"
pd1 := ct.SortASC
pf2 := "_id"
pd2 := ct.SortASC
pf1, pd1, pf2, pd2 := filter.Pagination() if filter != nil {
pipeline = filter.FilterQuery()
pf1, pd1, pf2, pd2 = filter.Pagination()
}
sortPrimary := pf1 sortPrimary := pf1
sortDirPrimary := pd1 sortDirPrimary := pd1
@@ -37,10 +45,19 @@ func (c *Coll[TData]) List(ctx context.Context, filter ct.Filter, pageSize *int,
return nil, ct.CursorToken{}, err return nil, ct.CursorToken{}, err
} }
// fast branch
if pageSize == nil {
entries, err := c.decodeAll(ctx, cursor)
if err != nil {
return nil, ct.CursorToken{}, err
}
return entries, ct.End(), nil
}
entities := make([]TData, 0, cursor.RemainingBatchLength()) entities := make([]TData, 0, cursor.RemainingBatchLength())
for (pageSize == nil || len(entities) != *pageSize) && cursor.Next(ctx) { for (pageSize == nil || len(entities) != *pageSize) && cursor.Next(ctx) {
var entry TData var entry TData
err = cursor.Decode(&entry) entry, err = c.decodeSingle(ctx, cursor)
if err != nil { if err != nil {
return nil, ct.CursorToken{}, err return nil, ct.CursorToken{}, err
} }
@@ -53,6 +70,8 @@ func (c *Coll[TData]) List(ctx context.Context, filter ct.Filter, pageSize *int,
last := entities[len(entities)-1] last := entities[len(entities)-1]
c.EnsureInitializedReflection(last)
nextToken, err := c.createToken(sortPrimary, sortDirPrimary, sortSecondary, sortDirSecondary, last, pageSize) nextToken, err := c.createToken(sortPrimary, sortDirPrimary, sortSecondary, sortDirSecondary, last, pageSize)
if err != nil { if err != nil {
return nil, ct.CursorToken{}, err return nil, ct.CursorToken{}, err

View File

@@ -8,14 +8,9 @@ import (
) )
func (c *Coll[TData]) FindOneAndUpdate(ctx context.Context, filterQuery bson.M, updateQuery bson.M) (TData, error) { func (c *Coll[TData]) FindOneAndUpdate(ctx context.Context, filterQuery bson.M, updateQuery bson.M) (TData, error) {
var res TData mongoRes := c.coll.FindOneAndUpdate(ctx, filterQuery, updateQuery, options.FindOneAndUpdate().SetReturnDocument(options.After))
err := c.coll.FindOneAndUpdate(ctx, filterQuery, updateQuery, options.FindOneAndUpdate().SetReturnDocument(options.After)).Decode(&res) return c.decodeSingle(ctx, mongoRes)
if err != nil {
return *new(TData), err
}
return res, nil
} }
func (c *Coll[TData]) UpdateOne(ctx context.Context, filterQuery bson.M, updateQuery bson.M) error { func (c *Coll[TData]) UpdateOne(ctx context.Context, filterQuery bson.M, updateQuery bson.M) error {
@@ -55,12 +50,7 @@ func (c *Coll[TData]) ReplaceOne(ctx context.Context, filterQuery bson.M, value
} }
func (c *Coll[TData]) FindOneAndReplace(ctx context.Context, filterQuery bson.M, value TData) (TData, error) { func (c *Coll[TData]) FindOneAndReplace(ctx context.Context, filterQuery bson.M, value TData) (TData, error) {
var res TData mongoRes := c.coll.FindOneAndUpdate(ctx, filterQuery, bson.M{"$set": value}, options.FindOneAndUpdate().SetReturnDocument(options.After))
err := c.coll.FindOneAndUpdate(ctx, filterQuery, bson.M{"$set": value}, options.FindOneAndUpdate().SetReturnDocument(options.After)).Decode(&res) return c.decodeSingle(ctx, mongoRes)
if err != nil {
return *new(TData), err
}
return res, nil
} }

View File

@@ -1,25 +1,62 @@
package wmo package wmo
import ( import (
"errors"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/reflectext" "gogs.mikescher.com/BlackForestBytes/goext/reflectext"
"reflect" "reflect"
"strings" "strings"
) )
func (c *Coll[TData]) init() { func (c *Coll[TData]) EnsureInitializedReflection(v TData) {
c.dataTypeMap = make(map[string]fullTypeRef[TData]) if !c.isInterfaceDataType {
return // only dynamically load dataTypeMap on interface TData
}
rval := reflect.ValueOf(v)
for rval.Type().Kind() == reflect.Pointer {
rval = rval.Elem()
}
if _, ok := c.implDataTypeMap[rval.Type()]; ok {
return // already loaded
}
m := make(map[string]fullTypeRef)
c.initFields("", rval, m, make([]int, 0))
c.implDataTypeMap[rval.Type()] = m
}
func (c *Coll[TData]) init() {
example := *new(TData) example := *new(TData)
v := reflect.ValueOf(example) datatype := reflect.TypeOf(&example).Elem()
c.initFields("", v, make([]int, 0)) if datatype.Kind() == reflect.Interface {
c.isInterfaceDataType = true
c.dataTypeMap = make(map[string]fullTypeRef)
c.implDataTypeMap = make(map[reflect.Type]map[string]fullTypeRef)
} else {
c.isInterfaceDataType = false
c.dataTypeMap = make(map[string]fullTypeRef)
c.implDataTypeMap = make(map[reflect.Type]map[string]fullTypeRef)
v := reflect.ValueOf(example)
c.initFields("", v, c.dataTypeMap, make([]int, 0))
}
} }
func (c *Coll[TData]) initFields(prefix string, rval reflect.Value, idxarr []int) { func (c *Coll[TData]) initFields(prefix string, rval reflect.Value, m map[string]fullTypeRef, idxarr []int) {
rtyp := rval.Type() rtyp := rval.Type()
@@ -32,50 +69,65 @@ func (c *Coll[TData]) initFields(prefix string, rval reflect.Value, idxarr []int
continue continue
} }
bsontags := make([]string, 0)
bsonkey, found := rsfield.Tag.Lookup("bson") bsonkey, found := rsfield.Tag.Lookup("bson")
if !found { if !found {
continue continue
} }
if strings.Contains(bsonkey, ",") { if strings.Contains(bsonkey, ",") {
bsontags = strings.Split(bsonkey[strings.Index(bsonkey, ",")+1:], ",")
bsonkey = bsonkey[:strings.Index(bsonkey, ",")] bsonkey = bsonkey[:strings.Index(bsonkey, ",")]
} }
if bsonkey == "-" { if bsonkey == "-" {
continue continue
} }
if bsonkey == "" {
bsonkey = rsfield.Name
}
fullKey := prefix + bsonkey fullKey := prefix + bsonkey
newIdxArr := langext.ArrCopy(idxarr) newIdxArr := langext.ArrCopy(idxarr)
newIdxArr = append(newIdxArr, i) newIdxArr = append(newIdxArr, i)
if rvfield.Type().Kind() == reflect.Pointer { if langext.InArray("inline", bsontags) && rvfield.Kind() == reflect.Struct {
c.dataTypeMap[fullKey] = fullTypeRef[TData]{ // pass-through field
IsPointer: true, c.initFields(prefix, rvfield, m, newIdxArr)
RealType: rvfield.Type(),
Kind: rvfield.Type().Elem().Kind(),
Type: rvfield.Type().Elem(),
UnderlyingType: reflectext.Underlying(rvfield.Type().Elem()),
Name: rsfield.Name,
Index: newIdxArr,
}
} else { } else {
c.dataTypeMap[fullKey] = fullTypeRef[TData]{ if rvfield.Type().Kind() == reflect.Pointer {
IsPointer: false,
RealType: rvfield.Type(), m[fullKey] = fullTypeRef{
Kind: rvfield.Type().Kind(), IsPointer: true,
Type: rvfield.Type(), RealType: rvfield.Type(),
UnderlyingType: reflectext.Underlying(rvfield.Type()), Kind: rvfield.Type().Elem().Kind(),
Name: rsfield.Name, Type: rvfield.Type().Elem(),
Index: newIdxArr, UnderlyingType: reflectext.Underlying(rvfield.Type().Elem()),
Name: rsfield.Name,
Index: newIdxArr,
}
} else {
m[fullKey] = fullTypeRef{
IsPointer: false,
RealType: rvfield.Type(),
Kind: rvfield.Type().Kind(),
Type: rvfield.Type(),
UnderlyingType: reflectext.Underlying(rvfield.Type()),
Name: rsfield.Name,
Index: newIdxArr,
}
} }
} if rvfield.Kind() == reflect.Struct {
c.initFields(fullKey+".", rvfield, m, newIdxArr)
}
if rvfield.Kind() == reflect.Struct {
c.initFields(fullKey+".", rvfield, newIdxArr)
} }
} }
@@ -84,7 +136,10 @@ func (c *Coll[TData]) initFields(prefix string, rval reflect.Value, idxarr []int
func (c *Coll[TData]) getTokenValueAsMongoType(value string, fieldName string) (any, error) { func (c *Coll[TData]) getTokenValueAsMongoType(value string, fieldName string) (any, error) {
fref := c.dataTypeMap[fieldName] fref, err := c.getFieldType(fieldName)
if err != nil {
return nil, err
}
pss := reflectext.PrimitiveStringSerializer{} pss := reflectext.PrimitiveStringSerializer{}
@@ -94,7 +149,10 @@ func (c *Coll[TData]) getTokenValueAsMongoType(value string, fieldName string) (
func (c *Coll[TData]) getFieldValueAsTokenString(entity TData, fieldName string) (string, error) { func (c *Coll[TData]) getFieldValueAsTokenString(entity TData, fieldName string) (string, error) {
realValue := c.getFieldValue(entity, fieldName) realValue, err := c.getFieldValue(entity, fieldName)
if err != nil {
return "", err
}
pss := reflectext.PrimitiveStringSerializer{} pss := reflectext.PrimitiveStringSerializer{}
@@ -102,12 +160,56 @@ func (c *Coll[TData]) getFieldValueAsTokenString(entity TData, fieldName string)
} }
func (c *Coll[TData]) getFieldType(fieldName string) fullTypeRef[TData] { func (c *Coll[TData]) getFieldType(fieldName string) (fullTypeRef, error) {
return c.dataTypeMap[fieldName] if c.isInterfaceDataType {
for _, m := range c.implDataTypeMap {
if r, ok := m[fieldName]; ok {
return r, nil
}
}
return fullTypeRef{}, errors.New("unknown field: '" + fieldName + "' (in any impl)")
} else {
if r, ok := c.dataTypeMap[fieldName]; ok {
return r, nil
} else {
return fullTypeRef{}, errors.New("unknown field: '" + fieldName + "'")
}
}
} }
func (c *Coll[TData]) getFieldValue(data TData, fieldName string) any { func (c *Coll[TData]) getFieldValue(data TData, fieldName string) (any, error) {
fref := c.dataTypeMap[fieldName] if c.isInterfaceDataType {
rval := reflect.ValueOf(data)
return rval.FieldByIndex(fref.Index).Interface() rval := reflect.ValueOf(data)
for rval.Type().Kind() == reflect.Pointer {
rval = rval.Elem()
}
if m, ok := c.implDataTypeMap[rval.Type()]; ok {
if fref, ok := m[fieldName]; ok {
rval := reflect.ValueOf(data)
return rval.FieldByIndex(fref.Index).Interface(), nil
} else {
return nil, errors.New("unknown bson field '" + fieldName + "' in type '" + rval.Type().String() + "'")
}
} else {
return nil, errors.New("unknown TData type: '" + rval.Type().String() + "'")
}
} else {
if fref, ok := c.dataTypeMap[fieldName]; ok {
rval := reflect.ValueOf(data)
return rval.FieldByIndex(fref.Index).Interface(), nil
} else {
return nil, errors.New("unknown bson field '" + fieldName + "'")
}
}
} }

View File

@@ -1,11 +1,14 @@
package wmo package wmo
import ( import (
"context"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/rfctime" "gogs.mikescher.com/BlackForestBytes/goext/rfctime"
"gogs.mikescher.com/BlackForestBytes/goext/timeext" "gogs.mikescher.com/BlackForestBytes/goext/timeext"
"gogs.mikescher.com/BlackForestBytes/goext/tst" "gogs.mikescher.com/BlackForestBytes/goext/tst"
"reflect"
"testing" "testing"
"time" "time"
) )
@@ -45,35 +48,51 @@ func TestReflectionGetFieldType(t *testing.T) {
MDate: t1, MDate: t1,
} }
tst.AssertEqual(t, coll.getFieldType("_id").Kind.String(), "string") gft := func(k string) fullTypeRef {
tst.AssertEqual(t, coll.getFieldType("_id").Type.String(), "wmo.IDType") v, err := coll.getFieldType(k)
tst.AssertEqual(t, coll.getFieldType("_id").Name, "ID") if err != nil {
tst.AssertEqual(t, coll.getFieldType("_id").IsPointer, false) t.Errorf("%s: %v", "failed to getFieldType", err)
tst.AssertEqual(t, coll.getFieldValue(d, "_id").(IDType), "1") }
return v
}
tst.AssertEqual(t, coll.getFieldType("cdate").Kind.String(), "struct") gfv := func(k string) any {
tst.AssertEqual(t, coll.getFieldType("cdate").Type.String(), "time.Time") v, err := coll.getFieldValue(d, k)
tst.AssertEqual(t, coll.getFieldType("cdate").Name, "CDate") if err != nil {
tst.AssertEqual(t, coll.getFieldType("cdate").IsPointer, false) t.Errorf("%s: %v", "failed to getFieldType", err)
tst.AssertEqual(t, coll.getFieldValue(d, "cdate").(time.Time), t0) }
return v
}
tst.AssertEqual(t, coll.getFieldType("sub.a").Kind.String(), "string") tst.AssertEqual(t, gft("_id").Kind.String(), "string")
tst.AssertEqual(t, coll.getFieldType("sub.a").Type.String(), "string") tst.AssertEqual(t, gft("_id").Type.String(), "wmo.IDType")
tst.AssertEqual(t, coll.getFieldType("sub.a").Name, "A") tst.AssertEqual(t, gft("_id").Name, "ID")
tst.AssertEqual(t, coll.getFieldType("sub.a").IsPointer, false) tst.AssertEqual(t, gft("_id").IsPointer, false)
tst.AssertEqual(t, coll.getFieldValue(d, "sub.a").(string), "2") tst.AssertEqual(t, gfv("_id").(IDType), "1")
tst.AssertEqual(t, coll.getFieldType("str").Kind.String(), "string") tst.AssertEqual(t, gft("cdate").Kind.String(), "struct")
tst.AssertEqual(t, coll.getFieldType("str").Type.String(), "string") tst.AssertEqual(t, gft("cdate").Type.String(), "time.Time")
tst.AssertEqual(t, coll.getFieldType("str").Name, "Str") tst.AssertEqual(t, gft("cdate").Name, "CDate")
tst.AssertEqual(t, coll.getFieldType("str").IsPointer, false) tst.AssertEqual(t, gft("cdate").IsPointer, false)
tst.AssertEqual(t, coll.getFieldValue(d, "str").(string), "3") tst.AssertEqual(t, gfv("cdate").(time.Time), t0)
tst.AssertEqual(t, coll.getFieldType("ptr").Kind.String(), "int") tst.AssertEqual(t, gft("sub.a").Kind.String(), "string")
tst.AssertEqual(t, coll.getFieldType("ptr").Type.String(), "int") tst.AssertEqual(t, gft("sub.a").Type.String(), "string")
tst.AssertEqual(t, coll.getFieldType("ptr").Name, "Ptr") tst.AssertEqual(t, gft("sub.a").Name, "A")
tst.AssertEqual(t, coll.getFieldType("ptr").IsPointer, true) tst.AssertEqual(t, gft("sub.a").IsPointer, false)
tst.AssertEqual(t, *coll.getFieldValue(d, "ptr").(*int), 4) tst.AssertEqual(t, gfv("sub.a").(string), "2")
tst.AssertEqual(t, gft("str").Kind.String(), "string")
tst.AssertEqual(t, gft("str").Type.String(), "string")
tst.AssertEqual(t, gft("str").Name, "Str")
tst.AssertEqual(t, gft("str").IsPointer, false)
tst.AssertEqual(t, gfv("str").(string), "3")
tst.AssertEqual(t, gft("ptr").Kind.String(), "int")
tst.AssertEqual(t, gft("ptr").Type.String(), "int")
tst.AssertEqual(t, gft("ptr").Name, "Ptr")
tst.AssertEqual(t, gft("ptr").IsPointer, true)
tst.AssertEqual(t, *gfv("ptr").(*int), 4)
} }
func TestReflectionGetTokenValueAsMongoType(t *testing.T) { func TestReflectionGetTokenValueAsMongoType(t *testing.T) {
@@ -177,3 +196,31 @@ func TestReflectionGetFieldValueAsTokenString(t *testing.T) {
tst.AssertEqual(t, gfvats(d, "cdate"), t0.Format(time.RFC3339Nano)) tst.AssertEqual(t, gfvats(d, "cdate"), t0.Format(time.RFC3339Nano))
tst.AssertEqual(t, gfvats(d, "mdate"), t0.Format(time.RFC3339Nano)) tst.AssertEqual(t, gfvats(d, "mdate"), t0.Format(time.RFC3339Nano))
} }
func TestReflectionWithInterface(t *testing.T) {
type TestData struct {
ID primitive.ObjectID `bson:"_id"`
CDate time.Time `bson:"cdate"`
}
type TestInterface interface {
}
coll1 := W[TestInterface](&mongo.Collection{})
tst.AssertTrue(t, coll1.coll != nil)
tst.AssertEqual(t, 0, len(coll1.implDataTypeMap))
df := func(ctx context.Context, dec Decodable) (TestInterface, error) {
return TestData{}, nil
}
coll2 := W[TestInterface](&mongo.Collection{}).WithDecodeFunc(df, TestData{})
tst.AssertTrue(t, coll2.coll != nil)
tst.AssertEqual(t, 1, len(coll2.implDataTypeMap))
tst.AssertEqual(t, "ID", coll2.implDataTypeMap[reflect.TypeOf(TestData{})]["_id"].Name)
tst.AssertEqual(t, "CDate", coll2.implDataTypeMap[reflect.TypeOf(TestData{})]["cdate"].Name)
}