Compare commits

..

79 Commits

Author SHA1 Message Date
c0443af63b exerr [WIP] 2023-02-15 16:04:19 +01:00
17383894a7 copy bmerr stuff from bm 2023-02-08 18:45:31 +01:00
fd33b43f31 v0.0.78 2023-02-03 01:05:36 +01:00
be4de07eb8 v0.0.77 2023-02-03 00:59:54 +01:00
36ed474bfe v0.0.76 2023-01-31 23:46:35 +01:00
fdc590c8c3 v0.0.75 2023-01-31 22:41:12 +01:00
1990e5d32d v0.0.74 2023-01-31 11:01:45 +01:00
72883cf6bd v0.0.73 2023-01-31 10:56:30 +01:00
ff08d5f180 v0.0.72 2023-01-30 19:55:55 +01:00
72d6b538f7 v0.0.71 2023-01-29 22:28:08 +01:00
48dd30fb94 v0.0.70 2023-01-29 22:07:28 +01:00
b7c5756f11 v0.0.69 2023-01-29 22:00:40 +01:00
2070a432a5 v0.0.68 2023-01-29 21:27:55 +01:00
34e6d1819d v0.0.67 2023-01-29 20:42:02 +01:00
87fa6021e4 v0.0.66 2023-01-29 06:02:58 +01:00
297d6c52a8 v0.0.65 2023-01-29 05:45:29 +01:00
b9c46947d2 v0.0.64 2023-01-29 01:10:14 +01:00
412277b3e0 v0.0.63 2023-01-28 22:29:45 +01:00
e46f8019ec v0.0.62 2023-01-28 22:29:21 +01:00
ae952b2166 v0.0.61 2023-01-28 22:28:20 +01:00
b24dba9a45 v0.0.60 2023-01-28 14:44:12 +01:00
cfbc20367d v0.0.59 2023-01-15 02:27:08 +01:00
e25912758e v0.0.58 2023-01-15 02:05:05 +01:00
e1ae77a9db v0.0.57 2023-01-15 01:56:40 +01:00
9d07b3955f v0.0.56 2023-01-13 16:05:39 +01:00
02be696c25 v0.0.55 2023-01-06 02:02:22 +01:00
ba07625b7c v0.0.54 2022-12-29 22:52:52 +01:00
aeded3fb37 v0.0.53 2022-12-24 03:11:09 +01:00
1a1cd6d0aa v0.0.52 2022-12-24 02:50:46 +01:00
64cc1342a0 v0.0.51 2022-12-24 01:14:58 +01:00
8431b6adf5 v0.0.50 2022-12-23 20:08:59 +01:00
24e923fe84 v0.0.49 2022-12-23 19:11:18 +01:00
10ddc7c190 v0.0.48 2022-12-23 14:47:16 +01:00
7f88a0726c v0.0.47 2022-12-23 10:11:01 +01:00
2224db8e85 v0.0.46 2022-12-22 15:59:12 +01:00
c60afc89bb v0.0.45 2022-12-22 15:55:32 +01:00
bbb33e9fd6 v0.0.44 2022-12-22 15:49:10 +01:00
ac05eff1e8 v0.0.43 2022-12-22 10:23:34 +01:00
1aaad66233 v0.0.42 2022-12-22 10:06:25 +01:00
d4994b8c8d v0.0.41 2022-12-21 15:41:41 +01:00
e3b8d2cc0f v0.0.40 2022-12-21 15:34:59 +01:00
fff609db4a v0.0.39 2022-12-21 14:39:59 +01:00
5e99e07f40 v0.0.38 2022-12-21 13:00:39 +01:00
bdb181cb3a v0.0.37 2022-12-20 09:50:13 +01:00
3552acd38b v0.0.36 2022-12-15 12:36:24 +01:00
c42324c58f v0.0.35 2022-12-14 18:25:07 +01:00
3a9c3f4e9e v0.0.34 2022-12-11 03:12:02 +01:00
becd8f1ebc v0.0.33 2022-12-11 02:34:38 +01:00
e733f30c38 v0.0.32 2022-12-10 03:33:13 +01:00
1a9e5c70fc v0.0.31 2022-12-07 23:25:21 +01:00
f3700a772d v0.0.30 2022-12-07 23:21:36 +01:00
2c69b33547 v0.0.29 2022-12-07 23:15:16 +01:00
6a304b875a v0.0.28 2022-11-30 23:52:19 +01:00
d12bf23b46 v0.0.27 2022-11-30 23:40:59 +01:00
52f7f6e690 v0.0.26 2022-11-30 23:38:42 +01:00
b1e3891256 v0.0.25 2022-11-30 23:35:47 +01:00
bdf5b53c20 v0.0.24 2022-11-30 23:34:16 +01:00
496c4e4f59 v0.0.23 2022-11-30 23:08:50 +01:00
deab986caf v0.0.22 2022-11-30 22:09:54 +01:00
9d9a6f1c6e v0.0.21 2022-11-19 16:58:18 +01:00
06a37f37b7 v0.0.20 2022-11-19 16:26:45 +01:00
b35d6ca0b0 v0.0.19 2022-11-19 13:34:21 +01:00
b643bded8a ArrFirstIndex, ArrLastIndex, ArrMap, ArrSum 2022-11-14 20:42:50 +01:00
93be4f9fdd added more supported GOOS 2022-10-29 15:44:51 +02:00
85b6d17df0 Tests && gomod 2022-10-29 15:34:40 +02:00
c7924cd9ff fix GOOS=windows / GOOS=darwin builds 2022-10-29 15:20:20 +02:00
07712aa08c fix timeext under 32bit compilation 2022-10-29 15:11:08 +02:00
f5243503db Add GeoDistance 2022-10-27 18:57:24 +02:00
21ae9c70d2 Added langext/coords && CompareArr[T] 2022-10-27 18:04:20 +02:00
c223e2f0fa added base62 2022-10-27 17:55:27 +02:00
a694f36f46 add tz params to timeext 2022-10-27 17:50:28 +02:00
e61682b24c Added termext.CleanString 2022-10-27 17:16:39 +02:00
5dc9e98f6b Add langext.ArrFirst / langext.ArrLast 2022-10-27 17:09:48 +02:00
4dd1c08e77 Add langext.BoolCount / langext.Range 2022-10-27 17:06:16 +02:00
c9e459edac Add mathext.Min / mathext.Max 2022-10-27 17:03:30 +02:00
3717eeb515 copy langext & termext from ffsclient 2022-10-27 16:48:26 +02:00
0eaeb5ac4f fix go mod (2) 2022-10-27 16:14:15 +02:00
c921e542eb fix go mod 2022-10-27 16:13:09 +02:00
47f123b86f remove ginext/mongoext (no-dep lib) 2022-10-27 16:07:42 +02:00
108 changed files with 7689 additions and 522 deletions

View File

@@ -1,6 +1,7 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="GoMixedReceiverTypes" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="LanguageDetectionInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
<option name="processCode" value="true" />

9
Makefile Normal file
View File

@@ -0,0 +1,9 @@
run:
echo "This is a library - can't be run" && false
test:
go test ./...
version:
_data/version.sh

View File

@@ -3,5 +3,6 @@ BFB goext library
A collection of general & useful library methods
Every subfolder is a seperate dependency and can be imported individually
This should not have any heavy dependencies (gin, mongo, etc) and add missing basic language features...
Potentially needs `export GOPRIVATE="gogs.mikescher.com"`

27
_data/version.sh Executable file
View File

@@ -0,0 +1,27 @@
#!/bin/bash
set -o nounset # disallow usage of unset vars ( set -u )
set -o errexit # Exit immediately if a pipeline returns non-zero. ( set -e )
set -o errtrace # Allow the above trap be inherited by all functions in the script. ( set -E )
set -o pipefail # Return value of a pipeline is the value of the last (rightmost) command to exit with a non-zero status
IFS=$'\n\t' # Set $IFS to only newline and tab.
curr_vers=$(git describe --tags --abbrev=0 | sed 's/v//g')
next_ver=$(echo "$curr_vers" | awk -F. -v OFS=. 'NF==1{print ++$NF}; NF>1{if(length($NF+1)>length($NF))$(NF-1)++; $NF=sprintf("%0*d", length($NF), ($NF+1)%(10^length($NF))); print}')
echo ""
echo "> Current Version: ${curr_vers}"
echo "> Next Version: ${next_ver}"
echo ""
git add --verbose .
git commit -a -m "v${next_ver}"
git tag "v${next_ver}"
git push
git push --tags

73
cmdext/builder.go Normal file
View File

@@ -0,0 +1,73 @@
package cmdext
import (
"fmt"
"time"
)
type CommandRunner struct {
program string
args []string
timeout *time.Duration
env []string
listener []CommandListener
}
func Runner(program string) *CommandRunner {
return &CommandRunner{
program: program,
args: make([]string, 0),
timeout: nil,
env: make([]string, 0),
listener: make([]CommandListener, 0),
}
}
func (r *CommandRunner) Arg(arg string) *CommandRunner {
r.args = append(r.args, arg)
return r
}
func (r *CommandRunner) Args(arg []string) *CommandRunner {
r.args = append(r.args, arg...)
return r
}
func (r *CommandRunner) Timeout(timeout time.Duration) *CommandRunner {
r.timeout = &timeout
return r
}
func (r *CommandRunner) Env(key, value string) *CommandRunner {
r.env = append(r.env, fmt.Sprintf("%s=%s", key, value))
return r
}
func (r *CommandRunner) RawEnv(env string) *CommandRunner {
r.env = append(r.env, env)
return r
}
func (r *CommandRunner) Envs(env []string) *CommandRunner {
r.env = append(r.env, env...)
return r
}
func (r *CommandRunner) Listen(lstr CommandListener) *CommandRunner {
r.listener = append(r.listener, lstr)
return r
}
func (r *CommandRunner) ListenStdout(lstr func(string)) *CommandRunner {
r.listener = append(r.listener, genericCommandListener{_readStdoutLine: &lstr})
return r
}
func (r *CommandRunner) ListenStderr(lstr func(string)) *CommandRunner {
r.listener = append(r.listener, genericCommandListener{_readStderrLine: &lstr})
return r
}
func (r *CommandRunner) Run() (CommandResult, error) {
return run(*r)
}

129
cmdext/cmdrunner.go Normal file
View File

@@ -0,0 +1,129 @@
package cmdext
import (
"gogs.mikescher.com/BlackForestBytes/goext/mathext"
"gogs.mikescher.com/BlackForestBytes/goext/syncext"
"os/exec"
"time"
)
type CommandResult struct {
StdOut string
StdErr string
StdCombined string
ExitCode int
CommandTimedOut bool
}
func run(opt CommandRunner) (CommandResult, error) {
cmd := exec.Command(opt.program, opt.args...)
cmd.Env = append(cmd.Env, opt.env...)
stdoutPipe, err := cmd.StdoutPipe()
if err != nil {
return CommandResult{}, err
}
stderrPipe, err := cmd.StderrPipe()
if err != nil {
return CommandResult{}, err
}
preader := pipeReader{
stdout: stdoutPipe,
stderr: stderrPipe,
}
err = cmd.Start()
if err != nil {
return CommandResult{}, err
}
type resultObj struct {
stdout string
stderr string
stdcombined string
err error
}
outputChan := make(chan resultObj)
go func() {
// we need to first fully read the pipes and then call Wait
// see https://pkg.go.dev/os/exec#Cmd.StdoutPipe
stdout, stderr, stdcombined, err := preader.Read(opt.listener)
if err != nil {
outputChan <- resultObj{stdout, stderr, stdcombined, err}
}
err = cmd.Wait()
if err != nil {
outputChan <- resultObj{stdout, stderr, stdcombined, err}
}
outputChan <- resultObj{stdout, stderr, stdcombined, nil}
}()
var timeoutChan <-chan time.Time = make(chan time.Time, 1)
if opt.timeout != nil {
timeoutChan = time.After(*opt.timeout)
}
select {
case <-timeoutChan:
_ = cmd.Process.Kill()
for _, lstr := range opt.listener {
lstr.Timeout()
}
if fallback, ok := syncext.ReadChannelWithTimeout(outputChan, mathext.Min(32*time.Millisecond, *opt.timeout)); ok {
// most of the time the cmd.Process.Kill() should also ahve finished the pipereader
// and we can at least return the already collected stdout, stderr, etc
return CommandResult{
StdOut: fallback.stdout,
StdErr: fallback.stderr,
StdCombined: fallback.stdcombined,
ExitCode: -1,
CommandTimedOut: true,
}, nil
} else {
return CommandResult{
StdOut: "",
StdErr: "",
StdCombined: "",
ExitCode: -1,
CommandTimedOut: true,
}, nil
}
case outobj := <-outputChan:
if exiterr, ok := outobj.err.(*exec.ExitError); ok {
excode := exiterr.ExitCode()
for _, lstr := range opt.listener {
lstr.Finished(excode)
}
return CommandResult{
StdOut: outobj.stdout,
StdErr: outobj.stderr,
StdCombined: outobj.stdcombined,
ExitCode: excode,
CommandTimedOut: false,
}, nil
} else if err != nil {
return CommandResult{}, err
} else {
for _, lstr := range opt.listener {
lstr.Finished(0)
}
return CommandResult{
StdOut: outobj.stdout,
StdErr: outobj.stderr,
StdCombined: outobj.stdcombined,
ExitCode: 0,
CommandTimedOut: false,
}, nil
}
}
}

226
cmdext/cmdrunner_test.go Normal file
View File

@@ -0,0 +1,226 @@
package cmdext
import (
"fmt"
"testing"
"time"
)
func TestStdout(t *testing.T) {
res1, err := Runner("printf").Arg("hello").Run()
if err != nil {
t.Errorf("%v", err)
}
if res1.StdErr != "" {
t.Errorf("res1.StdErr == '%v'", res1.StdErr)
}
if res1.StdOut != "hello" {
t.Errorf("res1.StdOut == '%v'", res1.StdOut)
}
if res1.StdCombined != "hello\n" {
t.Errorf("res1.StdCombined == '%v'", res1.StdCombined)
}
}
func TestStderr(t *testing.T) {
res1, err := Runner("python").Arg("-c").Arg("import sys; print(\"error\", file=sys.stderr, end='')").Run()
if err != nil {
t.Errorf("%v", err)
}
if res1.StdErr != "error" {
t.Errorf("res1.StdErr == '%v'", res1.StdErr)
}
if res1.StdOut != "" {
t.Errorf("res1.StdOut == '%v'", res1.StdOut)
}
if res1.StdCombined != "error\n" {
t.Errorf("res1.StdCombined == '%v'", res1.StdCombined)
}
}
func TestStdcombined(t *testing.T) {
res1, err := Runner("python").
Arg("-c").
Arg("import sys; import time; print(\"1\", file=sys.stderr, flush=True); time.sleep(0.1); print(\"2\", file=sys.stdout, flush=True); time.sleep(0.1); print(\"3\", file=sys.stderr, flush=True)").
Run()
if err != nil {
t.Errorf("%v", err)
}
if res1.StdErr != "1\n3\n" {
t.Errorf("res1.StdErr == '%v'", res1.StdErr)
}
if res1.StdOut != "2\n" {
t.Errorf("res1.StdOut == '%v'", res1.StdOut)
}
if res1.StdCombined != "1\n2\n3\n" {
t.Errorf("res1.StdCombined == '%v'", res1.StdCombined)
}
}
func TestPartialRead(t *testing.T) {
res1, err := Runner("python").
Arg("-c").
Arg("import sys; import time; print(\"first message\", flush=True); time.sleep(5); print(\"cant see me\", flush=True);").
Timeout(100 * time.Millisecond).
Run()
if err != nil {
t.Errorf("%v", err)
}
if !res1.CommandTimedOut {
t.Errorf("!CommandTimedOut")
}
if res1.StdErr != "" {
t.Errorf("res1.StdErr == '%v'", res1.StdErr)
}
if res1.StdOut != "first message\n" {
t.Errorf("res1.StdOut == '%v'", res1.StdOut)
}
if res1.StdCombined != "first message\n" {
t.Errorf("res1.StdCombined == '%v'", res1.StdCombined)
}
}
func TestPartialReadStderr(t *testing.T) {
res1, err := Runner("python").
Arg("-c").
Arg("import sys; import time; print(\"first message\", file=sys.stderr, flush=True); time.sleep(5); print(\"cant see me\", file=sys.stderr, flush=True);").
Timeout(100 * time.Millisecond).
Run()
if err != nil {
t.Errorf("%v", err)
}
if !res1.CommandTimedOut {
t.Errorf("!CommandTimedOut")
}
if res1.StdErr != "first message\n" {
t.Errorf("res1.StdErr == '%v'", res1.StdErr)
}
if res1.StdOut != "" {
t.Errorf("res1.StdOut == '%v'", res1.StdOut)
}
if res1.StdCombined != "first message\n" {
t.Errorf("res1.StdCombined == '%v'", res1.StdCombined)
}
}
func TestReadUnflushedStdout(t *testing.T) {
res1, err := Runner("python").Arg("-c").Arg("import sys; print(\"message101\", file=sys.stdout, end='')").Run()
if err != nil {
t.Errorf("%v", err)
}
if res1.StdErr != "" {
t.Errorf("res1.StdErr == '%v'", res1.StdErr)
}
if res1.StdOut != "message101" {
t.Errorf("res1.StdOut == '%v'", res1.StdOut)
}
if res1.StdCombined != "message101\n" {
t.Errorf("res1.StdCombined == '%v'", res1.StdCombined)
}
}
func TestReadUnflushedStderr(t *testing.T) {
res1, err := Runner("python").Arg("-c").Arg("import sys; print(\"message101\", file=sys.stderr, end='')").Run()
if err != nil {
t.Errorf("%v", err)
}
if res1.StdErr != "message101" {
t.Errorf("res1.StdErr == '%v'", res1.StdErr)
}
if res1.StdOut != "" {
t.Errorf("res1.StdOut == '%v'", res1.StdOut)
}
if res1.StdCombined != "message101\n" {
t.Errorf("res1.StdCombined == '%v'", res1.StdCombined)
}
}
func TestPartialReadUnflushed(t *testing.T) {
t.SkipNow()
res1, err := Runner("python").
Arg("-c").
Arg("import sys; import time; print(\"first message\", end=''); time.sleep(5); print(\"cant see me\", end='');").
Timeout(100 * time.Millisecond).
Run()
if err != nil {
t.Errorf("%v", err)
}
if !res1.CommandTimedOut {
t.Errorf("!CommandTimedOut")
}
if res1.StdErr != "" {
t.Errorf("res1.StdErr == '%v'", res1.StdErr)
}
if res1.StdOut != "first message" {
t.Errorf("res1.StdOut == '%v'", res1.StdOut)
}
if res1.StdCombined != "first message" {
t.Errorf("res1.StdCombined == '%v'", res1.StdCombined)
}
}
func TestPartialReadUnflushedStderr(t *testing.T) {
t.SkipNow()
res1, err := Runner("python").
Arg("-c").
Arg("import sys; import time; print(\"first message\", file=sys.stderr, end=''); time.sleep(5); print(\"cant see me\", file=sys.stderr, end='');").
Timeout(100 * time.Millisecond).
Run()
if err != nil {
t.Errorf("%v", err)
}
if !res1.CommandTimedOut {
t.Errorf("!CommandTimedOut")
}
if res1.StdErr != "first message" {
t.Errorf("res1.StdErr == '%v'", res1.StdErr)
}
if res1.StdOut != "" {
t.Errorf("res1.StdOut == '%v'", res1.StdOut)
}
if res1.StdCombined != "first message" {
t.Errorf("res1.StdCombined == '%v'", res1.StdCombined)
}
}
func TestListener(t *testing.T) {
_, err := Runner("python").
Arg("-c").
Arg("import sys;" +
"import time;" +
"print(\"message 1\", flush=True);" +
"time.sleep(1);" +
"print(\"message 2\", flush=True);" +
"time.sleep(1);" +
"print(\"message 3\", flush=True);" +
"time.sleep(1);" +
"print(\"message 4\", file=sys.stderr, flush=True);" +
"time.sleep(1);" +
"print(\"message 5\", flush=True);" +
"time.sleep(1);" +
"print(\"final\");").
ListenStdout(func(s string) { fmt.Printf("@@STDOUT <<- %v (%v)\n", s, time.Now().Format(time.RFC3339Nano)) }).
ListenStderr(func(s string) { fmt.Printf("@@STDERR <<- %v (%v)\n", s, time.Now().Format(time.RFC3339Nano)) }).
Timeout(10 * time.Second).
Run()
if err != nil {
panic(err)
}
}

12
cmdext/helper.go Normal file
View File

@@ -0,0 +1,12 @@
package cmdext
import "time"
func RunCommand(program string, args []string, timeout *time.Duration) (CommandResult, error) {
b := Runner(program)
b = b.Args(args)
if timeout != nil {
b = b.Timeout(*timeout)
}
return b.Run()
}

57
cmdext/listener.go Normal file
View File

@@ -0,0 +1,57 @@
package cmdext
type CommandListener interface {
ReadRawStdout([]byte)
ReadRawStderr([]byte)
ReadStdoutLine(string)
ReadStderrLine(string)
Finished(int)
Timeout()
}
type genericCommandListener struct {
_readRawStdout *func([]byte)
_readRawStderr *func([]byte)
_readStdoutLine *func(string)
_readStderrLine *func(string)
_finished *func(int)
_timeout *func()
}
func (g genericCommandListener) ReadRawStdout(v []byte) {
if g._readRawStdout != nil {
(*g._readRawStdout)(v)
}
}
func (g genericCommandListener) ReadRawStderr(v []byte) {
if g._readRawStderr != nil {
(*g._readRawStderr)(v)
}
}
func (g genericCommandListener) ReadStdoutLine(v string) {
if g._readStdoutLine != nil {
(*g._readStdoutLine)(v)
}
}
func (g genericCommandListener) ReadStderrLine(v string) {
if g._readStderrLine != nil {
(*g._readStderrLine)(v)
}
}
func (g genericCommandListener) Finished(v int) {
if g._finished != nil {
(*g._finished)(v)
}
}
func (g genericCommandListener) Timeout() {
if g._timeout != nil {
(*g._timeout)()
}
}

146
cmdext/pipereader.go Normal file
View File

@@ -0,0 +1,146 @@
package cmdext
import (
"bufio"
"gogs.mikescher.com/BlackForestBytes/goext/syncext"
"io"
"sync"
)
type pipeReader struct {
stdout io.ReadCloser
stderr io.ReadCloser
}
// Read ready stdout and stdin until finished
// also splits both pipes into lines and calld the listener
func (pr *pipeReader) Read(listener []CommandListener) (string, string, string, error) {
type combevt struct {
line string
stop bool
}
errch := make(chan error, 8)
wg := sync.WaitGroup{}
// [1] read raw stdout
wg.Add(1)
stdoutBufferReader, stdoutBufferWriter := io.Pipe()
stdout := ""
go func() {
buf := make([]byte, 128)
for true {
n, out := pr.stdout.Read(buf)
if n > 0 {
txt := string(buf[:n])
stdout += txt
_, _ = stdoutBufferWriter.Write(buf[:n])
for _, lstr := range listener {
lstr.ReadRawStdout(buf[:n])
}
}
if out == io.EOF {
break
}
if out != nil {
errch <- out
break
}
}
_ = stdoutBufferWriter.Close()
wg.Done()
}()
// [2] read raw stderr
wg.Add(1)
stderrBufferReader, stderrBufferWriter := io.Pipe()
stderr := ""
go func() {
buf := make([]byte, 128)
for true {
n, err := pr.stderr.Read(buf)
if n > 0 {
txt := string(buf[:n])
stderr += txt
_, _ = stderrBufferWriter.Write(buf[:n])
for _, lstr := range listener {
lstr.ReadRawStderr(buf[:n])
}
}
if err == io.EOF {
break
}
if err != nil {
errch <- err
break
}
}
_ = stderrBufferWriter.Close()
wg.Done()
}()
combch := make(chan combevt, 32)
// [3] collect stdout line-by-line
wg.Add(1)
go func() {
scanner := bufio.NewScanner(stdoutBufferReader)
for scanner.Scan() {
txt := scanner.Text()
for _, lstr := range listener {
lstr.ReadStdoutLine(txt)
}
combch <- combevt{txt, false}
}
combch <- combevt{"", true}
wg.Done()
}()
// [4] collect stderr line-by-line
wg.Add(1)
go func() {
scanner := bufio.NewScanner(stderrBufferReader)
for scanner.Scan() {
txt := scanner.Text()
for _, lstr := range listener {
lstr.ReadStderrLine(txt)
}
combch <- combevt{txt, false}
}
combch <- combevt{"", true}
wg.Done()
}()
// [5] combine stdcombined
wg.Add(1)
stdcombined := ""
go func() {
stopctr := 0
for stopctr < 2 {
vvv := <-combch
if vvv.stop {
stopctr++
} else {
stdcombined += vvv.line + "\n" // this comes from bufio.Scanner and has no newlines...
}
}
wg.Done()
}()
// wait for all (5) goroutines to finish
wg.Wait()
if err, ok := syncext.ReadNonBlocking(errch); ok {
return "", "", "", err
}
return stdout, stderr, stdcombined, nil
}

172
confext/confParser.go Normal file
View File

@@ -0,0 +1,172 @@
package confext
import (
"errors"
"fmt"
"gogs.mikescher.com/BlackForestBytes/goext/timeext"
"math/bits"
"os"
"reflect"
"strconv"
"time"
)
// ApplyEnvOverrides overrides field values from environment variables
//
// fields must be tagged with `env:"env_key"`
//
// only works on exported fields
//
// fields without an env tag are ignored
// fields with an `env:"-"` tag are ignore
//
// sub-structs are recursively parsed (if they have an env tag) and the env-variable keys are delimited by the delim parameter
// sub-structs with `env:""` are also parsed, but the delimited is skipped (they are handled as if they were one level higher)
func ApplyEnvOverrides[T any](c *T, delim string) error {
rval := reflect.ValueOf(c).Elem()
return processEnvOverrides(rval, delim, "")
}
func processEnvOverrides(rval reflect.Value, delim string, prefix string) error {
rtyp := rval.Type()
for i := 0; i < rtyp.NumField(); i++ {
rsfield := rtyp.Field(i)
rvfield := rval.Field(i)
if !rsfield.IsExported() {
continue
}
if rvfield.Kind() == reflect.Struct {
envkey, found := rsfield.Tag.Lookup("env")
if !found || envkey == "-" {
continue
}
subPrefix := prefix
if envkey != "" {
subPrefix = subPrefix + envkey + delim
}
err := processEnvOverrides(rvfield, delim, subPrefix)
if err != nil {
return err
}
}
envkey := rsfield.Tag.Get("env")
if envkey == "" || envkey == "-" {
continue
}
fullEnvKey := prefix + envkey
envval, efound := os.LookupEnv(fullEnvKey)
if !efound {
continue
}
if rvfield.Type() == reflect.TypeOf("") {
rvfield.Set(reflect.ValueOf(envval))
fmt.Printf("[CONF] Overwrite config '%s' with '%s'\n", fullEnvKey, envval)
} else if rvfield.Type() == reflect.TypeOf(int(0)) {
envint, err := strconv.ParseInt(envval, 10, bits.UintSize)
if err != nil {
return errors.New(fmt.Sprintf("Failed to parse env-config variable '%s' to int (value := '%s')", fullEnvKey, envval))
}
rvfield.Set(reflect.ValueOf(int(envint)))
fmt.Printf("[CONF] Overwrite config '%s' with '%s'\n", fullEnvKey, envval)
} else if rvfield.Type() == reflect.TypeOf(int64(0)) {
envint, err := strconv.ParseInt(envval, 10, 64)
if err != nil {
return errors.New(fmt.Sprintf("Failed to parse env-config variable '%s' to int64 (value := '%s')", fullEnvKey, envval))
}
rvfield.Set(reflect.ValueOf(int64(envint)))
fmt.Printf("[CONF] Overwrite config '%s' with '%s'\n", fullEnvKey, envval)
} else if rvfield.Type() == reflect.TypeOf(int32(0)) {
envint, err := strconv.ParseInt(envval, 10, 32)
if err != nil {
return errors.New(fmt.Sprintf("Failed to parse env-config variable '%s' to int32 (value := '%s')", fullEnvKey, envval))
}
rvfield.Set(reflect.ValueOf(int32(envint)))
fmt.Printf("[CONF] Overwrite config '%s' with '%s'\n", fullEnvKey, envval)
} else if rvfield.Type() == reflect.TypeOf(int8(0)) {
envint, err := strconv.ParseInt(envval, 10, 8)
if err != nil {
return errors.New(fmt.Sprintf("Failed to parse env-config variable '%s' to int32 (value := '%s')", fullEnvKey, envval))
}
rvfield.Set(reflect.ValueOf(int8(envint)))
fmt.Printf("[CONF] Overwrite config '%s' with '%s'\n", fullEnvKey, envval)
} else if rvfield.Type() == reflect.TypeOf(time.Duration(0)) {
dur, err := timeext.ParseDurationShortString(envval)
if err != nil {
return errors.New(fmt.Sprintf("Failed to parse env-config variable '%s' to duration (value := '%s')", fullEnvKey, envval))
}
rvfield.Set(reflect.ValueOf(dur))
fmt.Printf("[CONF] Overwrite config '%s' with '%s'\n", fullEnvKey, dur.String())
} else if rvfield.Type() == reflect.TypeOf(time.UnixMilli(0)) {
tim, err := time.Parse(time.RFC3339Nano, envval)
if err != nil {
return errors.New(fmt.Sprintf("Failed to parse env-config variable '%s' to time.time (value := '%s')", fullEnvKey, envval))
}
rvfield.Set(reflect.ValueOf(tim))
fmt.Printf("[CONF] Overwrite config '%s' with '%s'\n", fullEnvKey, tim.String())
} else if rvfield.Type().ConvertibleTo(reflect.TypeOf(int(0))) {
envint, err := strconv.ParseInt(envval, 10, 8)
if err != nil {
return errors.New(fmt.Sprintf("Failed to parse env-config variable '%s' to <%s, ,int> (value := '%s')", rvfield.Type().Name(), fullEnvKey, envval))
}
envcvl := reflect.ValueOf(envint).Convert(rvfield.Type())
rvfield.Set(envcvl)
fmt.Printf("[CONF] Overwrite config '%s' with '%v'\n", fullEnvKey, envcvl.Interface())
} else if rvfield.Type().ConvertibleTo(reflect.TypeOf("")) {
envcvl := reflect.ValueOf(envval).Convert(rvfield.Type())
rvfield.Set(envcvl)
fmt.Printf("[CONF] Overwrite config '%s' with '%v'\n", fullEnvKey, envcvl.Interface())
} else {
return errors.New(fmt.Sprintf("Unknown kind/type in config: [ %s | %s ]", rvfield.Kind().String(), rvfield.Type().String()))
}
}
return nil
}

220
confext/confParser_test.go Normal file
View File

@@ -0,0 +1,220 @@
package confext
import (
"gogs.mikescher.com/BlackForestBytes/goext/timeext"
"testing"
"time"
)
func TestApplyEnvOverridesNoop(t *testing.T) {
type aliasint int
type aliasstring string
type testdata struct {
V1 int `env:"TEST_V1"`
VX string ``
V2 string `env:"TEST_V2"`
V3 int8 `env:"TEST_V3"`
V4 int32 `env:"TEST_V4"`
V5 int64 `env:"TEST_V5"`
V6 aliasint `env:"TEST_V6"`
VY aliasint ``
V7 aliasstring `env:"TEST_V7"`
V8 time.Duration `env:"TEST_V8"`
V9 time.Time `env:"TEST_V9"`
}
input := testdata{
V1: 1,
VX: "X",
V2: "2",
V3: 3,
V4: 4,
V5: 5,
V6: 6,
VY: 99,
V7: "7",
V8: 9,
V9: time.Unix(1671102873, 0),
}
output := input
err := ApplyEnvOverrides(&output, ".")
if err != nil {
t.Errorf("%v", err)
t.FailNow()
}
assertEqual(t, input, output)
}
func TestApplyEnvOverridesSimple(t *testing.T) {
type aliasint int
type aliasstring string
type testdata struct {
V1 int `env:"TEST_V1"`
VX string ``
V2 string `env:"TEST_V2"`
V3 int8 `env:"TEST_V3"`
V4 int32 `env:"TEST_V4"`
V5 int64 `env:"TEST_V5"`
V6 aliasint `env:"TEST_V6"`
VY aliasint ``
V7 aliasstring `env:"TEST_V7"`
V8 time.Duration `env:"TEST_V8"`
V9 time.Time `env:"TEST_V9"`
}
data := testdata{
V1: 1,
VX: "X",
V2: "2",
V3: 3,
V4: 4,
V5: 5,
V6: 6,
VY: 99,
V7: "7",
V8: 9,
V9: time.Unix(1671102873, 0),
}
t.Setenv("TEST_V1", "846")
t.Setenv("TEST_V2", "hello_world")
t.Setenv("TEST_V3", "6")
t.Setenv("TEST_V4", "333")
t.Setenv("TEST_V5", "-937")
t.Setenv("TEST_V6", "070")
t.Setenv("TEST_V7", "AAAAAA")
t.Setenv("TEST_V8", "1min4s")
t.Setenv("TEST_V9", "2009-11-10T23:00:00Z")
err := ApplyEnvOverrides(&data, ".")
if err != nil {
t.Errorf("%v", err)
t.FailNow()
}
assertEqual(t, data.V1, 846)
assertEqual(t, data.V2, "hello_world")
assertEqual(t, data.V3, 6)
assertEqual(t, data.V4, 333)
assertEqual(t, data.V5, -937)
assertEqual(t, data.V6, 70)
assertEqual(t, data.V7, "AAAAAA")
assertEqual(t, data.V8, time.Second*64)
assertEqual(t, data.V9, time.Unix(1257894000, 0).UTC())
}
func TestApplyEnvOverridesRecursive(t *testing.T) {
type subdata struct {
V1 int `env:"SUB_V1"`
VX string ``
V2 string `env:"SUB_V2"`
V8 time.Duration `env:"SUB_V3"`
V9 time.Time `env:"SUB_V4"`
}
type testdata struct {
V1 int `env:"TEST_V1"`
VX string ``
Sub1 subdata ``
Sub2 subdata `env:"TEST_V2"`
Sub3 subdata `env:"TEST_V3"`
Sub4 subdata `env:""`
V5 string `env:"-"`
}
data := testdata{
V1: 1,
VX: "2",
V5: "no",
Sub1: subdata{
V1: 3,
VX: "4",
V2: "5",
V8: 6 * time.Second,
V9: time.Date(2000, 1, 7, 1, 1, 1, 0, time.UTC),
},
Sub2: subdata{
V1: 8,
VX: "9",
V2: "10",
V8: 11 * time.Second,
V9: time.Date(2000, 1, 12, 1, 1, 1, 0, timeext.TimezoneBerlin),
},
Sub3: subdata{
V1: 13,
VX: "14",
V2: "15",
V8: 16 * time.Second,
V9: time.Date(2000, 1, 17, 1, 1, 1, 0, timeext.TimezoneBerlin),
},
Sub4: subdata{
V1: 18,
VX: "19",
V2: "20",
V8: 21 * time.Second,
V9: time.Date(2000, 1, 22, 1, 1, 1, 0, timeext.TimezoneBerlin),
},
}
t.Setenv("TEST_V1", "999")
t.Setenv("-", "yes")
t.Setenv("TEST_V2_SUB_V1", "846")
t.Setenv("TEST_V2_SUB_V2", "222_hello_world")
t.Setenv("TEST_V2_SUB_V3", "1min4s")
t.Setenv("TEST_V2_SUB_V4", "2009-11-10T23:00:00Z")
t.Setenv("TEST_V3_SUB_V1", "33846")
t.Setenv("TEST_V3_SUB_V2", "33_hello_world")
t.Setenv("TEST_V3_SUB_V3", "33min4s")
t.Setenv("TEST_V3_SUB_V4", "2033-11-10T23:00:00Z")
t.Setenv("SUB_V1", "11")
t.Setenv("SUB_V2", "22")
t.Setenv("SUB_V3", "33min")
t.Setenv("SUB_V4", "2044-01-01T00:00:00Z")
err := ApplyEnvOverrides(&data, "_")
if err != nil {
t.Errorf("%v", err)
t.FailNow()
}
assertEqual(t, data.V1, 999)
assertEqual(t, data.VX, "2")
assertEqual(t, data.V5, "no")
assertEqual(t, data.Sub1.V1, 3)
assertEqual(t, data.Sub1.VX, "4")
assertEqual(t, data.Sub1.V2, "5")
assertEqual(t, data.Sub1.V8, time.Second*6)
assertEqual(t, data.Sub1.V9, time.Unix(947206861, 0).UTC())
assertEqual(t, data.Sub2.V1, 846)
assertEqual(t, data.Sub2.VX, "9")
assertEqual(t, data.Sub2.V2, "222_hello_world")
assertEqual(t, data.Sub2.V8, time.Second*64)
assertEqual(t, data.Sub2.V9, time.Unix(1257894000, 0).UTC())
assertEqual(t, data.Sub3.V1, 33846)
assertEqual(t, data.Sub3.VX, "14")
assertEqual(t, data.Sub3.V2, "33_hello_world")
assertEqual(t, data.Sub3.V8, time.Second*1984)
assertEqual(t, data.Sub3.V9, time.Unix(2015276400, 0).UTC())
assertEqual(t, data.Sub4.V1, 11)
assertEqual(t, data.Sub4.VX, "19")
assertEqual(t, data.Sub4.V2, "22")
assertEqual(t, data.Sub4.V8, time.Second*1980)
assertEqual(t, data.Sub4.V9, time.Unix(2335219200, 0).UTC())
}
func assertEqual[T comparable](t *testing.T, actual T, expected T) {
if actual != expected {
t.Errorf("values differ: Actual: '%v', Expected: '%v'", actual, expected)
}
}

132
cryptext/aes.go Normal file
View File

@@ -0,0 +1,132 @@
package cryptext
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"encoding/base32"
"encoding/json"
"errors"
"golang.org/x/crypto/scrypt"
"io"
)
// https://stackoverflow.com/a/18819040/1761622
type aesPayload struct {
Salt []byte `json:"s"`
IV []byte `json:"i"`
Data []byte `json:"d"`
Rounds int `json:"r"`
Version uint `json:"v"`
}
func EncryptAESSimple(password []byte, data []byte, rounds int) (string, error) {
salt := make([]byte, 8)
_, err := io.ReadFull(rand.Reader, salt)
if err != nil {
return "", err
}
key, err := scrypt.Key(password, salt, rounds, 8, 1, 32)
if err != nil {
return "", err
}
block, err := aes.NewCipher(key)
if err != nil {
return "", err
}
h := sha256.New()
h.Write(data)
checksum := h.Sum(nil)
if len(checksum) != 32 {
return "", errors.New("wrong cs size")
}
ciphertext := make([]byte, 32+len(data))
iv := make([]byte, aes.BlockSize)
_, err = io.ReadFull(rand.Reader, iv)
if err != nil {
return "", err
}
combinedData := make([]byte, 0, 32+len(data))
combinedData = append(combinedData, checksum...)
combinedData = append(combinedData, data...)
cfb := cipher.NewCFBEncrypter(block, iv)
cfb.XORKeyStream(ciphertext, combinedData)
pl := aesPayload{
Salt: salt,
IV: iv,
Data: ciphertext,
Version: 1,
Rounds: rounds,
}
jbin, err := json.Marshal(pl)
if err != nil {
return "", err
}
res := base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(jbin)
return res, nil
}
func DecryptAESSimple(password []byte, encText string) ([]byte, error) {
jbin, err := base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(encText)
if err != nil {
return nil, err
}
var pl aesPayload
err = json.Unmarshal(jbin, &pl)
if err != nil {
return nil, err
}
if pl.Version != 1 {
return nil, errors.New("unsupported version")
}
key, err := scrypt.Key(password, pl.Salt, pl.Rounds, 8, 1, 32) // this is not 100% correct, rounds too low and salt is missing
if err != nil {
return nil, err
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
dest := make([]byte, len(pl.Data))
cfb := cipher.NewCFBDecrypter(block, pl.IV)
cfb.XORKeyStream(dest, pl.Data)
if len(dest) < 32 {
return nil, errors.New("payload too small")
}
chck := dest[:32]
data := dest[32:]
h := sha256.New()
h.Write(data)
chck2 := h.Sum(nil)
if !bytes.Equal(chck, chck2) {
return nil, errors.New("checksum mismatch")
}
return data, nil
}

35
cryptext/aes_test.go Normal file
View File

@@ -0,0 +1,35 @@
package cryptext
import (
"fmt"
"testing"
)
func TestEncryptAESSimple(t *testing.T) {
pw := []byte("hunter12")
str1 := []byte("Hello World")
str2, err := EncryptAESSimple(pw, str1, 512)
if err != nil {
panic(err)
}
fmt.Printf("%s\n", str2)
str3, err := DecryptAESSimple(pw, str2)
if err != nil {
panic(err)
}
assertEqual(t, string(str1), string(str3))
str4, err := EncryptAESSimple(pw, str3, 512)
if err != nil {
panic(err)
}
assertNotEqual(t, string(str2), string(str4))
}

View File

@@ -23,3 +23,9 @@ func assertEqual(t *testing.T, actual string, expected string) {
t.Errorf("values differ: Actual: '%v', Expected: '%v'", actual, expected)
}
}
func assertNotEqual(t *testing.T, actual string, expected string) {
if actual == expected {
t.Errorf("values do not differ: Actual: '%v', Expected: '%v'", actual, expected)
}
}

365
cryptext/passHash.go Normal file
View File

@@ -0,0 +1,365 @@
package cryptext
import (
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/totpext"
"golang.org/x/crypto/bcrypt"
"strconv"
"strings"
)
const LatestPassHashVersion = 4
// PassHash
// - [v0]: plaintext password ( `0|...` )
// - [v1]: sha256(plaintext)
// - [v2]: seed | sha256<seed>(plaintext)
// - [v3]: seed | sha256<seed>(plaintext) | [hex(totp)]
// - [v4]: bcrypt(plaintext) | [hex(totp)]
type PassHash string
func (ph PassHash) Valid() bool {
_, _, _, _, _, valid := ph.Data()
return valid
}
func (ph PassHash) HasTOTP() bool {
_, _, _, otp, _, _ := ph.Data()
return otp
}
func (ph PassHash) Data() (_version int, _seed []byte, _payload []byte, _totp bool, _totpsecret []byte, _valid bool) {
split := strings.Split(string(ph), "|")
if len(split) == 0 {
return -1, nil, nil, false, nil, false
}
version, err := strconv.ParseInt(split[0], 10, 32)
if err != nil {
return -1, nil, nil, false, nil, false
}
if version == 0 {
if len(split) != 2 {
return -1, nil, nil, false, nil, false
}
return int(version), nil, []byte(split[1]), false, nil, true
}
if version == 1 {
if len(split) != 2 {
return -1, nil, nil, false, nil, false
}
payload, err := base64.RawStdEncoding.DecodeString(split[1])
if err != nil {
return -1, nil, nil, false, nil, false
}
return int(version), nil, payload, false, nil, true
}
//
if version == 2 {
if len(split) != 3 {
return -1, nil, nil, false, nil, false
}
seed, err := base64.RawStdEncoding.DecodeString(split[1])
if err != nil {
return -1, nil, nil, false, nil, false
}
payload, err := base64.RawStdEncoding.DecodeString(split[2])
if err != nil {
return -1, nil, nil, false, nil, false
}
return int(version), seed, payload, false, nil, true
}
if version == 3 {
if len(split) != 4 {
return -1, nil, nil, false, nil, false
}
seed, err := base64.RawStdEncoding.DecodeString(split[1])
if err != nil {
return -1, nil, nil, false, nil, false
}
payload, err := base64.RawStdEncoding.DecodeString(split[2])
if err != nil {
return -1, nil, nil, false, nil, false
}
totp := false
totpsecret := make([]byte, 0)
if split[3] != "0" {
totpsecret, err = hex.DecodeString(split[3])
totp = true
}
return int(version), seed, payload, totp, totpsecret, true
}
if version == 4 {
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[3])
totp = true
}
return int(version), nil, payload, totp, totpsecret, true
}
return -1, nil, nil, false, nil, false
}
func (ph PassHash) Verify(plainpass string, totp *string) bool {
version, seed, payload, hastotp, totpsecret, valid := ph.Data()
if !valid {
return false
}
if hastotp && totp == nil {
return false
}
if version == 0 {
return langext.ArrEqualsExact([]byte(plainpass), payload)
}
if version == 1 {
return langext.ArrEqualsExact(hash256(plainpass), payload)
}
if version == 2 {
return langext.ArrEqualsExact(hash256Seeded(plainpass, seed), payload)
}
if version == 3 {
if !hastotp {
return langext.ArrEqualsExact(hash256Seeded(plainpass, seed), payload)
} else {
return langext.ArrEqualsExact(hash256Seeded(plainpass, seed), payload) && totpext.Validate(totpsecret, *totp)
}
}
if version == 4 {
if !hastotp {
return bcrypt.CompareHashAndPassword(payload, []byte(plainpass)) == nil
} else {
return bcrypt.CompareHashAndPassword(payload, []byte(plainpass)) == nil && totpext.Validate(totpsecret, *totp)
}
}
return false
}
func (ph PassHash) NeedsPasswordUpgrade() bool {
version, _, _, _, _, valid := ph.Data()
return valid && version < LatestPassHashVersion
}
func (ph PassHash) Upgrade(plainpass string) (PassHash, error) {
version, _, _, hastotp, totpsecret, valid := ph.Data()
if !valid {
return "", errors.New("invalid password")
}
if version == LatestPassHashVersion {
return ph, nil
}
if hastotp {
return HashPassword(plainpass, totpsecret)
} else {
return HashPassword(plainpass, nil)
}
}
func (ph PassHash) ClearTOTP() (PassHash, error) {
version, _, _, _, _, valid := ph.Data()
if !valid {
return "", errors.New("invalid PassHash")
}
if version == 0 {
return ph, nil
}
if version == 1 {
return ph, nil
}
if version == 2 {
return ph, nil
}
if version == 3 {
split := strings.Split(string(ph), "|")
split[3] = "0"
return PassHash(strings.Join(split, "|")), nil
}
if version == 4 {
split := strings.Split(string(ph), "|")
split[2] = "0"
return PassHash(strings.Join(split, "|")), nil
}
return "", errors.New("unknown version")
}
func (ph PassHash) WithTOTP(totpSecret []byte) (PassHash, error) {
version, _, _, _, _, valid := ph.Data()
if !valid {
return "", errors.New("invalid PassHash")
}
if version == 0 {
return "", errors.New("version does not support totp, needs upgrade")
}
if version == 1 {
return "", errors.New("version does not support totp, needs upgrade")
}
if version == 2 {
return "", errors.New("version does not support totp, needs upgrade")
}
if version == 3 {
split := strings.Split(string(ph), "|")
split[3] = hex.EncodeToString(totpSecret)
return PassHash(strings.Join(split, "|")), nil
}
if version == 4 {
split := strings.Split(string(ph), "|")
split[2] = hex.EncodeToString(totpSecret)
return PassHash(strings.Join(split, "|")), nil
}
return "", errors.New("unknown version")
}
func (ph PassHash) Change(newPlainPass string) (PassHash, error) {
version, _, _, hastotp, totpsecret, valid := ph.Data()
if !valid {
return "", errors.New("invalid PassHash")
}
if version == 0 {
return HashPasswordV0(newPlainPass)
}
if version == 1 {
return HashPasswordV1(newPlainPass)
}
if version == 2 {
return HashPasswordV2(newPlainPass)
}
if version == 3 {
return HashPasswordV3(newPlainPass, langext.Conditional(hastotp, totpsecret, nil))
}
if version == 4 {
return HashPasswordV4(newPlainPass, langext.Conditional(hastotp, totpsecret, nil))
}
return "", errors.New("unknown version")
}
func (ph PassHash) String() string {
return string(ph)
}
func HashPassword(plainpass string, totpSecret []byte) (PassHash, error) {
return HashPasswordV4(plainpass, totpSecret)
}
func HashPasswordV4(plainpass string, totpSecret []byte) (PassHash, error) {
var strtotp string
if totpSecret == nil {
strtotp = "0"
} else {
strtotp = hex.EncodeToString(totpSecret)
}
payload, err := bcrypt.GenerateFromPassword([]byte(plainpass), bcrypt.MinCost)
if err != nil {
return "", err
}
return PassHash(fmt.Sprintf("4|%s|%s", string(payload), strtotp)), nil
}
func HashPasswordV3(plainpass string, totpSecret []byte) (PassHash, error) {
var strtotp string
if totpSecret == nil {
strtotp = "0"
} else {
strtotp = hex.EncodeToString(totpSecret)
}
seed, err := newSeed()
if err != nil {
return "", err
}
checksum := hash256Seeded(plainpass, seed)
return PassHash(fmt.Sprintf("3|%s|%s|%s",
base64.RawStdEncoding.EncodeToString(seed),
base64.RawStdEncoding.EncodeToString(checksum),
strtotp)), nil
}
func HashPasswordV2(plainpass string) (PassHash, error) {
seed, err := newSeed()
if err != nil {
return "", err
}
checksum := hash256Seeded(plainpass, seed)
return PassHash(fmt.Sprintf("2|%s|%s", base64.RawStdEncoding.EncodeToString(seed), base64.RawStdEncoding.EncodeToString(checksum))), nil
}
func HashPasswordV1(plainpass string) (PassHash, error) {
return PassHash(fmt.Sprintf("1|%s", base64.RawStdEncoding.EncodeToString(hash256(plainpass)))), nil
}
func HashPasswordV0(plainpass string) (PassHash, error) {
return PassHash(fmt.Sprintf("0|%s", plainpass)), nil
}
func hash256(s string) []byte {
h := sha256.New()
h.Write([]byte(s))
bs := h.Sum(nil)
return bs
}
func hash256Seeded(s string, seed []byte) []byte {
h := sha256.New()
h.Write(seed)
h.Write([]byte(s))
bs := h.Sum(nil)
return bs
}
func newSeed() ([]byte, error) {
secret := make([]byte, 32)
_, err := rand.Read(secret)
if err != nil {
return nil, err
}
return secret, nil
}

View File

@@ -0,0 +1,157 @@
package dataext
import (
"errors"
"io"
)
type brcMode int
const (
modeSourceReading brcMode = 0
modeSourceFinished brcMode = 1
modeBufferReading brcMode = 2
modeBufferFinished brcMode = 3
)
type BufferedReadCloser interface {
io.ReadCloser
BufferedAll() ([]byte, error)
Reset() error
}
type bufferedReadCloser struct {
buffer []byte
inner io.ReadCloser
mode brcMode
off int
}
func NewBufferedReadCloser(sub io.ReadCloser) BufferedReadCloser {
return &bufferedReadCloser{
buffer: make([]byte, 0, 1024),
inner: sub,
mode: modeSourceReading,
off: 0,
}
}
func (b *bufferedReadCloser) Read(p []byte) (int, error) {
switch b.mode {
case modeSourceReading:
n, err := b.inner.Read(p)
if n > 0 {
b.buffer = append(b.buffer, p[0:n]...)
}
if err == io.EOF {
b.mode = modeSourceFinished
}
return n, err
case modeSourceFinished:
return 0, io.EOF
case modeBufferReading:
if len(b.buffer) <= b.off {
b.mode = modeBufferFinished
if len(p) == 0 {
return 0, nil
}
return 0, io.EOF
}
n := copy(p, b.buffer[b.off:])
b.off += n
return n, nil
case modeBufferFinished:
return 0, io.EOF
default:
return 0, errors.New("object in undefined status")
}
}
func (b *bufferedReadCloser) Close() error {
switch b.mode {
case modeSourceReading:
_, err := b.BufferedAll()
if err != nil {
return err
}
err = b.inner.Close()
if err != nil {
return err
}
b.mode = modeSourceFinished
return nil
case modeSourceFinished:
return nil
case modeBufferReading:
b.mode = modeBufferFinished
return nil
case modeBufferFinished:
return nil
default:
return errors.New("object in undefined status")
}
}
func (b *bufferedReadCloser) BufferedAll() ([]byte, error) {
switch b.mode {
case modeSourceReading:
arr := make([]byte, 1024)
for b.mode == modeSourceReading {
_, err := b.Read(arr)
if err != nil && err != io.EOF {
return nil, err
}
}
return b.buffer, nil
case modeSourceFinished:
return b.buffer, nil
case modeBufferReading:
return b.buffer, nil
case modeBufferFinished:
return b.buffer, nil
default:
return nil, errors.New("object in undefined status")
}
}
func (b *bufferedReadCloser) Reset() error {
switch b.mode {
case modeSourceReading:
fallthrough
case modeSourceFinished:
err := b.Close()
if err != nil {
return err
}
b.mode = modeBufferReading
b.off = 0
return nil
case modeBufferReading:
fallthrough
case modeBufferFinished:
b.mode = modeBufferReading
b.off = 0
return nil
default:
return errors.New("object in undefined status")
}
}

View File

@@ -19,40 +19,38 @@ import (
// There are also a bunch of unit tests to ensure that the cache is always in a consistent state
//
type LRUData interface{}
type LRUMap struct {
type LRUMap[TKey comparable, TData any] struct {
maxsize int
lock sync.Mutex
cache map[string]*cacheNode
cache map[TKey]*cacheNode[TKey, TData]
lfuHead *cacheNode
lfuTail *cacheNode
lfuHead *cacheNode[TKey, TData]
lfuTail *cacheNode[TKey, TData]
}
type cacheNode struct {
key string
data LRUData
parent *cacheNode
child *cacheNode
type cacheNode[TKey comparable, TData any] struct {
key TKey
data TData
parent *cacheNode[TKey, TData]
child *cacheNode[TKey, TData]
}
func NewLRUMap(size int) *LRUMap {
func NewLRUMap[TKey comparable, TData any](size int) *LRUMap[TKey, TData] {
if size <= 2 && size != 0 {
panic("Size must be > 2 (or 0)")
}
return &LRUMap{
return &LRUMap[TKey, TData]{
maxsize: size,
lock: sync.Mutex{},
cache: make(map[string]*cacheNode, size+1),
cache: make(map[TKey]*cacheNode[TKey, TData], size+1),
lfuHead: nil,
lfuTail: nil,
}
}
func (c *LRUMap) Put(key string, value LRUData) {
func (c *LRUMap[TKey, TData]) Put(key TKey, value TData) {
if c.maxsize == 0 {
return // cache disabled
}
@@ -70,7 +68,7 @@ func (c *LRUMap) Put(key string, value LRUData) {
}
// key does not exist: insert into map and add to top of LFU
node = &cacheNode{
node = &cacheNode[TKey, TData]{
key: key,
data: value,
parent: nil,
@@ -95,9 +93,9 @@ func (c *LRUMap) Put(key string, value LRUData) {
}
}
func (c *LRUMap) TryGet(key string) (LRUData, bool) {
func (c *LRUMap[TKey, TData]) TryGet(key TKey) (TData, bool) {
if c.maxsize == 0 {
return nil, false // cache disabled
return *new(TData), false // cache disabled
}
c.lock.Lock()
@@ -105,13 +103,13 @@ func (c *LRUMap) TryGet(key string) (LRUData, bool) {
val, ok := c.cache[key]
if !ok {
return nil, false
return *new(TData), false
}
c.moveNodeToTop(val)
return val.data, ok
}
func (c *LRUMap) moveNodeToTop(node *cacheNode) {
func (c *LRUMap[TKey, TData]) moveNodeToTop(node *cacheNode[TKey, TData]) {
// (only called in critical section !)
if c.lfuHead == node { // fast case
@@ -144,7 +142,7 @@ func (c *LRUMap) moveNodeToTop(node *cacheNode) {
}
}
func (c *LRUMap) Size() int {
func (c *LRUMap[TKey, TData]) Size() int {
c.lock.Lock()
defer c.lock.Unlock()
return len(c.cache)

View File

@@ -1,7 +1,7 @@
package dataext
import (
"go.mongodb.org/mongo-driver/bson/primitive"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"math/rand"
"strconv"
"testing"
@@ -12,7 +12,7 @@ func init() {
}
func TestResultCache1(t *testing.T) {
cache := NewLRUMap(8)
cache := NewLRUMap[string](8)
verifyLRUList(cache, t)
key := randomKey()
@@ -39,7 +39,7 @@ func TestResultCache1(t *testing.T) {
if !ok {
t.Errorf("cache TryGet returned no value")
}
if !eq(cacheval, val) {
if cacheval != val {
t.Errorf("cache TryGet returned different value (%+v <> %+v)", cacheval, val)
}
@@ -50,7 +50,7 @@ func TestResultCache1(t *testing.T) {
}
func TestResultCache2(t *testing.T) {
cache := NewLRUMap(8)
cache := NewLRUMap[string](8)
verifyLRUList(cache, t)
key1 := "key1"
@@ -150,7 +150,7 @@ func TestResultCache2(t *testing.T) {
}
func TestResultCache3(t *testing.T) {
cache := NewLRUMap(8)
cache := NewLRUMap[string](8)
verifyLRUList(cache, t)
key1 := "key1"
@@ -160,20 +160,20 @@ func TestResultCache3(t *testing.T) {
cache.Put(key1, val1)
verifyLRUList(cache, t)
if val, ok := cache.TryGet(key1); !ok || !eq(val, val1) {
if val, ok := cache.TryGet(key1); !ok || val != val1 {
t.Errorf("Value in cache should be [val1]")
}
cache.Put(key1, val2)
verifyLRUList(cache, t)
if val, ok := cache.TryGet(key1); !ok || !eq(val, val2) {
if val, ok := cache.TryGet(key1); !ok || val != val2 {
t.Errorf("Value in cache should be [val2]")
}
}
// does a basic consistency check over the internal cache representation
func verifyLRUList(cache *LRUMap, t *testing.T) {
func verifyLRUList[TData any](cache *LRUMap[TData], t *testing.T) {
size := 0
tailFound := false
@@ -250,20 +250,10 @@ func randomKey() string {
return strconv.FormatInt(rand.Int63(), 16)
}
func randomVal() LRUData {
v := primitive.NewObjectID()
return &v
}
func eq(a LRUData, b LRUData) bool {
v1, ok1 := a.(*primitive.ObjectID)
v2, ok2 := b.(*primitive.ObjectID)
if ok1 && ok2 {
if v1 == nil || v2 == nil {
return false
}
return v1.Hex() == v2.Hex()
func randomVal() string {
v, err := langext.NewHexUUID()
if err != nil {
panic(err)
}
return false
return v
}

35
dataext/merge.go Normal file
View File

@@ -0,0 +1,35 @@
package dataext
import (
"reflect"
)
func ObjectMerge[T1 any, T2 any](base T1, override T2) T1 {
reflBase := reflect.ValueOf(&base).Elem()
reflOvrd := reflect.ValueOf(&override).Elem()
for i := 0; i < reflBase.NumField(); i++ {
fieldBase := reflBase.Field(i)
fieldOvrd := reflOvrd.Field(i)
if fieldBase.Kind() != reflect.Ptr || fieldOvrd.Kind() != reflect.Ptr {
continue
}
kindBase := fieldBase.Type().Elem().Kind()
kindOvrd := fieldOvrd.Type().Elem().Kind()
if kindBase != kindOvrd {
continue
}
if !fieldOvrd.IsNil() {
fieldBase.Set(fieldOvrd.Elem().Addr())
}
}
return base
}

70
dataext/merge_test.go Normal file
View File

@@ -0,0 +1,70 @@
package dataext
import (
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"testing"
)
func TestObjectMerge(t *testing.T) {
type A struct {
Field1 *int
Field2 *string
Field3 *float64
Field4 *bool
OnlyA int64
DiffType int
}
type B struct {
Field1 *int
Field2 *string
Field3 *float64
Field4 *bool
OnlyB int64
DiffType string
}
valueA := A{
Field1: nil,
Field2: langext.Ptr("99"),
Field3: langext.Ptr(12.2),
Field4: nil,
OnlyA: 1,
DiffType: 2,
}
valueB := B{
Field1: langext.Ptr(12),
Field2: nil,
Field3: langext.Ptr(13.2),
Field4: nil,
OnlyB: 1,
DiffType: "X",
}
valueMerge := ObjectMerge(valueA, valueB)
assertPtrEqual(t, "Field1", valueMerge.Field1, valueB.Field1)
assertPtrEqual(t, "Field2", valueMerge.Field2, valueA.Field2)
assertPtrEqual(t, "Field3", valueMerge.Field3, valueB.Field3)
assertPtrEqual(t, "Field4", valueMerge.Field4, nil)
}
func assertPtrEqual[T1 comparable](t *testing.T, ident string, actual *T1, expected *T1) {
if actual == nil && expected == nil {
return
}
if actual != nil && expected != nil {
if *actual != *expected {
t.Errorf("[%s] values differ: Actual: '%v', Expected: '%v'", ident, *actual, *expected)
} else {
return
}
}
if actual == nil && expected != nil {
t.Errorf("[%s] values differ: Actual: nil, Expected: not-nil", ident)
}
if actual != nil && expected == nil {
t.Errorf("[%s] values differ: Actual: not-nil, Expected: nil", ident)
}
}

116
dataext/stack.go Normal file
View File

@@ -0,0 +1,116 @@
package dataext
import (
"errors"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"sync"
)
var ErrEmptyStack = errors.New("stack is empty")
type Stack[T any] struct {
lock *sync.Mutex
data []T
}
func NewStack[T any](threadsafe bool, initialCapacity int) *Stack[T] {
var lck *sync.Mutex = nil
if threadsafe {
lck = &sync.Mutex{}
}
return &Stack[T]{
lock: lck,
data: make([]T, 0, initialCapacity),
}
}
func (s *Stack[T]) Push(v T) {
if s.lock != nil {
s.lock.Lock()
defer s.lock.Unlock()
}
s.data = append(s.data, v)
}
func (s *Stack[T]) Pop() (T, error) {
if s.lock != nil {
s.lock.Lock()
defer s.lock.Unlock()
}
l := len(s.data)
if l == 0 {
return *new(T), ErrEmptyStack
}
result := s.data[l-1]
s.data = s.data[:l-1]
return result, nil
}
func (s *Stack[T]) OptPop() *T {
if s.lock != nil {
s.lock.Lock()
defer s.lock.Unlock()
}
l := len(s.data)
if l == 0 {
return nil
}
result := s.data[l-1]
s.data = s.data[:l-1]
return langext.Ptr(result)
}
func (s *Stack[T]) Peek() (T, error) {
if s.lock != nil {
s.lock.Lock()
defer s.lock.Unlock()
}
l := len(s.data)
if l == 0 {
return *new(T), ErrEmptyStack
}
return s.data[l-1], nil
}
func (s *Stack[T]) OptPeek() *T {
if s.lock != nil {
s.lock.Lock()
defer s.lock.Unlock()
}
l := len(s.data)
if l == 0 {
return nil
}
return langext.Ptr(s.data[l-1])
}
func (s *Stack[T]) Length() int {
if s.lock != nil {
s.lock.Lock()
defer s.lock.Unlock()
}
return len(s.data)
}
func (s *Stack[T]) Empty() bool {
if s.lock != nil {
s.lock.Lock()
defer s.lock.Unlock()
}
return len(s.data) == 0
}

254
dataext/structHash.go Normal file
View File

@@ -0,0 +1,254 @@
package dataext
import (
"bytes"
"crypto/sha256"
"encoding/binary"
"errors"
"fmt"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"hash"
"io"
"reflect"
"sort"
)
type StructHashOptions struct {
HashAlgo hash.Hash
Tag *string
SkipChannel bool
SkipFunc bool
}
func StructHash(dat any, opt ...StructHashOptions) (r []byte, err error) {
defer func() {
if rec := recover(); rec != nil {
r = nil
err = errors.New(fmt.Sprintf("recovered panic: %v", rec))
}
}()
shopt := StructHashOptions{}
if len(opt) > 1 {
return nil, errors.New("multiple options supplied")
} else if len(opt) == 1 {
shopt = opt[0]
}
if shopt.HashAlgo == nil {
shopt.HashAlgo = sha256.New()
}
writer := new(bytes.Buffer)
if langext.IsNil(dat) {
shopt.HashAlgo.Reset()
shopt.HashAlgo.Write(writer.Bytes())
res := shopt.HashAlgo.Sum(nil)
return res, nil
}
err = binarize(writer, reflect.ValueOf(dat), shopt)
if err != nil {
return nil, err
}
shopt.HashAlgo.Reset()
shopt.HashAlgo.Write(writer.Bytes())
res := shopt.HashAlgo.Sum(nil)
return res, nil
}
func writeBinarized(writer io.Writer, dat any) error {
tmp := bytes.Buffer{}
err := binary.Write(&tmp, binary.LittleEndian, dat)
if err != nil {
return err
}
err = binary.Write(writer, binary.LittleEndian, uint64(tmp.Len()))
if err != nil {
return err
}
_, err = writer.Write(tmp.Bytes())
if err != nil {
return err
}
return nil
}
func binarize(writer io.Writer, dat reflect.Value, opt StructHashOptions) error {
var err error
err = binary.Write(writer, binary.LittleEndian, uint8(dat.Kind()))
switch dat.Kind() {
case reflect.Ptr, reflect.Map, reflect.Array, reflect.Chan, reflect.Slice, reflect.Interface:
if dat.IsNil() {
err = binary.Write(writer, binary.LittleEndian, uint64(0))
if err != nil {
return err
}
return nil
}
}
err = binary.Write(writer, binary.LittleEndian, uint64(len(dat.Type().String())))
if err != nil {
return err
}
_, err = writer.Write([]byte(dat.Type().String()))
if err != nil {
return err
}
switch dat.Type().Kind() {
case reflect.Invalid:
return errors.New("cannot binarize value of kind <Invalid>")
case reflect.Bool:
return writeBinarized(writer, dat.Bool())
case reflect.Int:
return writeBinarized(writer, int64(dat.Int()))
case reflect.Int8:
fallthrough
case reflect.Int16:
fallthrough
case reflect.Int32:
fallthrough
case reflect.Int64:
return writeBinarized(writer, dat.Interface())
case reflect.Uint:
return writeBinarized(writer, uint64(dat.Int()))
case reflect.Uint8:
fallthrough
case reflect.Uint16:
fallthrough
case reflect.Uint32:
fallthrough
case reflect.Uint64:
return writeBinarized(writer, dat.Interface())
case reflect.Uintptr:
return errors.New("cannot binarize value of kind <Uintptr>")
case reflect.Float32:
fallthrough
case reflect.Float64:
return writeBinarized(writer, dat.Interface())
case reflect.Complex64:
return errors.New("cannot binarize value of kind <Complex64>")
case reflect.Complex128:
return errors.New("cannot binarize value of kind <Complex128>")
case reflect.Slice:
fallthrough
case reflect.Array:
return binarizeArrayOrSlice(writer, dat, opt)
case reflect.Chan:
if opt.SkipChannel {
return nil
}
return errors.New("cannot binarize value of kind <Chan>")
case reflect.Func:
if opt.SkipFunc {
return nil
}
return errors.New("cannot binarize value of kind <Func>")
case reflect.Interface:
return binarize(writer, dat.Elem(), opt)
case reflect.Map:
return binarizeMap(writer, dat, opt)
case reflect.Pointer:
return binarize(writer, dat.Elem(), opt)
case reflect.String:
v := dat.String()
err = binary.Write(writer, binary.LittleEndian, uint64(len(v)))
if err != nil {
return err
}
_, err = writer.Write([]byte(v))
if err != nil {
return err
}
return nil
case reflect.Struct:
return binarizeStruct(writer, dat, opt)
case reflect.UnsafePointer:
return errors.New("cannot binarize value of kind <UnsafePointer>")
default:
return errors.New("cannot binarize value of unknown kind <" + dat.Type().Kind().String() + ">")
}
}
func binarizeStruct(writer io.Writer, dat reflect.Value, opt StructHashOptions) error {
err := binary.Write(writer, binary.LittleEndian, uint64(dat.NumField()))
if err != nil {
return err
}
for i := 0; i < dat.NumField(); i++ {
if opt.Tag != nil {
if _, ok := dat.Type().Field(i).Tag.Lookup(*opt.Tag); !ok {
continue
}
}
err = binary.Write(writer, binary.LittleEndian, uint64(len(dat.Type().Field(i).Name)))
if err != nil {
return err
}
_, err = writer.Write([]byte(dat.Type().Field(i).Name))
if err != nil {
return err
}
err = binarize(writer, dat.Field(i), opt)
if err != nil {
return err
}
}
return nil
}
func binarizeArrayOrSlice(writer io.Writer, dat reflect.Value, opt StructHashOptions) error {
err := binary.Write(writer, binary.LittleEndian, uint64(dat.Len()))
if err != nil {
return err
}
for i := 0; i < dat.Len(); i++ {
err := binarize(writer, dat.Index(i), opt)
if err != nil {
return err
}
}
return nil
}
func binarizeMap(writer io.Writer, dat reflect.Value, opt StructHashOptions) error {
err := binary.Write(writer, binary.LittleEndian, uint64(dat.Len()))
if err != nil {
return err
}
sub := make([][]byte, 0, dat.Len())
for _, k := range dat.MapKeys() {
tmp := bytes.Buffer{}
err = binarize(&tmp, dat.MapIndex(k), opt)
if err != nil {
return err
}
sub = append(sub, tmp.Bytes())
}
sort.Slice(sub, func(i1, i2 int) bool { return bytes.Compare(sub[i1], sub[i2]) < 0 })
for _, v := range sub {
_, err = writer.Write(v)
if err != nil {
return err
}
}
return nil
}

143
dataext/structHash_test.go Normal file
View File

@@ -0,0 +1,143 @@
package dataext
import (
"encoding/hex"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"testing"
)
func noErrStructHash(t *testing.T, dat any, opt ...StructHashOptions) []byte {
res, err := StructHash(dat, opt...)
if err != nil {
t.Error(err)
t.FailNow()
return nil
}
return res
}
func TestStructHashSimple(t *testing.T) {
assertEqual(t, "209bf774af36cc3a045c152d9f1269ef3684ad819c1359ee73ff0283a308fefa", noErrStructHash(t, "Hello"))
assertEqual(t, "c32f3626b981ae2997db656f3acad3f1dc9d30ef6b6d14296c023e391b25f71a", noErrStructHash(t, 0))
assertEqual(t, "01b781b03e9586b257d387057dfc70d9f06051e7d3c1e709a57e13cc8daf3e35", noErrStructHash(t, []byte{}))
assertEqual(t, "93e1dcd45c732fe0079b0fb3204c7c803f0921835f6bfee2e6ff263e73eed53c", noErrStructHash(t, []int{}))
assertEqual(t, "54f637a376aad55b3160d98ebbcae8099b70d91b9400df23fb3709855d59800a", noErrStructHash(t, []int{1, 2, 3}))
assertEqual(t, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", noErrStructHash(t, nil))
assertEqual(t, "349a7db91aa78fd30bbaa7c7f9c7bfb2fcfe72869b4861162a96713a852f60d3", noErrStructHash(t, []any{1, "", nil}))
assertEqual(t, "ca51aab87808bf0062a4a024de6aac0c2bad54275cc857a4944569f89fd245ad", noErrStructHash(t, struct{}{}))
}
func TestStructHashSimpleStruct(t *testing.T) {
type t0 struct {
F1 int
F2 []string
F3 *int
}
assertEqual(t, "a90bff751c70c738bb5cfc9b108e783fa9c19c0bc9273458e0aaee6e74aa1b92", noErrStructHash(t, t0{
F1: 10,
F2: []string{"1", "2", "3"},
F3: nil,
}))
assertEqual(t, "5d09090dc34ac59dd645f197a255f653387723de3afa1b614721ea5a081c675f", noErrStructHash(t, t0{
F1: 10,
F2: []string{"1", "2", "3"},
F3: langext.Ptr(99),
}))
}
func TestStructHashLayeredStruct(t *testing.T) {
type t1_1 struct {
F10 float32
F12 float64
F15 bool
}
type t1_2 struct {
SV1 *t1_1
SV2 *t1_1
SV3 t1_1
}
assertEqual(t, "fd4ca071fb40a288fee4b7a3dfdaab577b30cb8f80f81ec511e7afd72dc3b469", noErrStructHash(t, t1_2{
SV1: nil,
SV2: nil,
SV3: t1_1{
F10: 1,
F12: 2,
F15: false,
},
}))
assertEqual(t, "3fbf7c67d8121deda075cc86319a4e32d71744feb2cebf89b43bc682f072a029", noErrStructHash(t, t1_2{
SV1: nil,
SV2: &t1_1{},
SV3: t1_1{
F10: 3,
F12: 4,
F15: true,
},
}))
assertEqual(t, "b1791ccd1b346c3ede5bbffda85555adcd8216b93ffca23f14fe175ec47c5104", noErrStructHash(t, t1_2{
SV1: &t1_1{},
SV2: &t1_1{},
SV3: t1_1{
F10: 5,
F12: 6,
F15: false,
},
}))
}
func TestStructHashMap(t *testing.T) {
type t0 struct {
F1 int
F2 map[string]int
}
assertEqual(t, "d50c53ad1fafb448c33fddd5aca01a86a2edf669ce2ecab07ba6fe877951d824", noErrStructHash(t, t0{
F1: 10,
F2: map[string]int{
"x": 1,
"0": 2,
"a": 99,
},
}))
assertEqual(t, "d50c53ad1fafb448c33fddd5aca01a86a2edf669ce2ecab07ba6fe877951d824", noErrStructHash(t, t0{
F1: 10,
F2: map[string]int{
"a": 99,
"x": 1,
"0": 2,
},
}))
m3 := make(map[string]int, 99)
m3["a"] = 0
m3["x"] = 0
m3["0"] = 0
m3["0"] = 99
m3["x"] = 1
m3["a"] = 2
assertEqual(t, "d50c53ad1fafb448c33fddd5aca01a86a2edf669ce2ecab07ba6fe877951d824", noErrStructHash(t, t0{
F1: 10,
F2: m3,
}))
}
func assertEqual(t *testing.T, expected string, actual []byte) {
actualStr := hex.EncodeToString(actual)
if actualStr != expected {
t.Errorf("values differ: Actual: '%v', Expected: '%v'", actualStr, expected)
}
}

View File

@@ -2,17 +2,17 @@ package dataext
import "sync"
type SyncStringSet struct {
data map[string]bool
type SyncSet[TData comparable] struct {
data map[TData]bool
lock sync.Mutex
}
func (s *SyncStringSet) Add(value string) bool {
func (s *SyncSet[TData]) Add(value TData) bool {
s.lock.Lock()
defer s.lock.Unlock()
if s.data == nil {
s.data = make(map[string]bool)
s.data = make(map[TData]bool)
}
_, ok := s.data[value]
@@ -21,12 +21,12 @@ func (s *SyncStringSet) Add(value string) bool {
return !ok
}
func (s *SyncStringSet) AddAll(values []string) {
func (s *SyncSet[TData]) AddAll(values []TData) {
s.lock.Lock()
defer s.lock.Unlock()
if s.data == nil {
s.data = make(map[string]bool)
s.data = make(map[TData]bool)
}
for _, value := range values {
@@ -34,12 +34,12 @@ func (s *SyncStringSet) AddAll(values []string) {
}
}
func (s *SyncStringSet) Contains(value string) bool {
func (s *SyncSet[TData]) Contains(value TData) bool {
s.lock.Lock()
defer s.lock.Unlock()
if s.data == nil {
s.data = make(map[string]bool)
s.data = make(map[TData]bool)
}
_, ok := s.data[value]
@@ -47,15 +47,15 @@ func (s *SyncStringSet) Contains(value string) bool {
return ok
}
func (s *SyncStringSet) Get() []string {
func (s *SyncSet[TData]) Get() []TData {
s.lock.Lock()
defer s.lock.Unlock()
if s.data == nil {
s.data = make(map[string]bool)
s.data = make(map[TData]bool)
}
r := make([]string, 0, len(s.data))
r := make([]TData, 0, len(s.data))
for k := range s.data {
r = append(r, k)

View File

View File

@@ -1,3 +0,0 @@
module blackforestbytes.com/goext/error
go 1.19

416
exerr/builder.go Normal file
View File

@@ -0,0 +1,416 @@
package exerr
import (
"bytes"
"encoding/json"
"fmt"
"github.com/rs/zerolog"
"go.mongodb.org/mongo-driver/bson/primitive"
"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 bmerror:
// value, err := do_something(..)
// if err != nil {
// return nil, bmerror.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, bmerror.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, bmerror.New(bmerror.ErrInternal, "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 {
bmerror *bringmanError
containsGinData bool
}
func Get(err error) *Builder {
return &Builder{bmerror: fromError(err)}
}
func New(t ErrorType, msg string) *Builder {
return &Builder{bmerror: newBringmanErr(CatSystem, t, msg)}
}
func Wrap(err error, msg string) *Builder {
return &Builder{bmerror: fromError(err).wrap(msg, CatWrap, 1)}
}
// ----------------------------------------------------------------------------
func (b *Builder) WithType(t ErrorType) *Builder {
b.bmerror.Type = t
return b
}
func (b *Builder) WithStatuscode(status int) *Builder {
b.bmerror.StatusCode = status
return b
}
func (b *Builder) WithMessage(msg string) *Builder {
b.bmerror.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.bmerror.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.bmerror.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.bmerror.Severity = SevInfo
return b
}
// ----------------------------------------------------------------------------
// User sets the Category to CatUser
//
// Errors with category
func (b *Builder) User() *Builder {
b.bmerror.Category = CatUser
return b
}
func (b *Builder) System() *Builder {
b.bmerror.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).toBMError().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.(langext.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 {
if b.bmerror.Severity == SevErr || b.bmerror.Severity == SevFatal {
b.bmerror.ShortLog(stackSkipLogger.Error())
}
b.CallListener(MethodBuild)
return b.bmerror.ToGrpcError()
}
// 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.bmerror.Output(ctx, g)
if b.bmerror.Severity == SevErr || b.bmerror.Severity == SevFatal {
b.bmerror.Log(stackSkipLogger.Error())
} else if b.bmerror.Severity == SevWarn {
b.bmerror.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.bmerror.Severity == SevErr || b.bmerror.Severity == SevFatal {
b.bmerror.Log(stackSkipLogger.Error())
} else if b.bmerror.Severity == SevWarn {
b.bmerror.ShortLog(stackSkipLogger.Warn())
}
b.CallListener(MethodPrint)
}
func (b *Builder) Format(level LogPrintLevel) string {
return b.bmerror.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.bmerror.Severity = SevFatal
b.bmerror.Log(stackSkipLogger.WithLevel(zerolog.FatalLevel))
b.CallListener(MethodFatal)
os.Exit(1)
}
// ----------------------------------------------------------------------------
func (b *Builder) addMeta(key string, mdtype metaDataType, val interface{}) *Builder {
b.bmerror.Meta.add(key, mdtype, val)
return b
}
func (b *Builder) toBMError() BMError {
return b.bmerror.ToBMError()
}

32
exerr/data.go Normal file
View File

@@ -0,0 +1,32 @@
package exerr
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
)
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"}
)
var AllSeverities = []ErrorSeverity{SevTrace, SevDebug, SevInfo, SevWarn, SevErr, SevFatal}
type ErrorType struct{ Key string }
var (
TypeInternal = ErrorType{"Internal"}
// other values come from pkgconfig
)

1
exerr/defaults.go Normal file
View File

@@ -0,0 +1 @@
package exerr

50
exerr/errinit.go Normal file
View File

@@ -0,0 +1,50 @@
package exerr
type ErrorPackageConfig struct {
ZeroLogTraces bool // autom print zerolog logs on CreateError
RecursiveErrors bool // errors contains their Origin-Error
Types []ErrorType // all available error-types
}
type ErrorPackageConfigInit struct {
LogTraces bool
RecursiveErrors bool
InitTypes func(_ func(_ string) ErrorType)
}
var initialized = false
var pkgconfig = ErrorPackageConfig{
ZeroLogTraces: true,
RecursiveErrors: true,
Types: []ErrorType{TypeInternal},
}
// 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")
}
types := pkgconfig.Types
fnAddType := func(v string) ErrorType {
et := ErrorType{v}
types = append(types, et)
return et
}
if cfg.InitTypes != nil {
cfg.InitTypes(fnAddType)
}
pkgconfig = ErrorPackageConfig{
ZeroLogTraces: cfg.LogTraces,
RecursiveErrors: cfg.RecursiveErrors,
Types: types,
}
initialized = true
}

33
exerr/exerr.go Normal file
View File

@@ -0,0 +1,33 @@
package exerr
import (
"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"`
Message string `json:"message"`
Caller string `json:"caller"`
OriginalError *ExErr
Meta MetaMap `json:"meta"`
}
func (ee ExErr) Error() string {
}
func (ee ExErr) Unwrap() error {
}
func (ee ExErr) Is(err error) bool {
}

146
exerr/foreign.go Normal file
View File

@@ -0,0 +1,146 @@
package exerr
import (
"bringman.de/common/shared/langext"
"encoding/json"
"go.mongodb.org/mongo-driver/bson/primitive"
"reflect"
"time"
)
var reflectTypeStr = reflect.TypeOf("")
func getForeignMeta(err error) (mm MetaMap) {
mm = make(map[string]MetaValue)
defer func() {
if panicerr := recover(); panicerr != nil {
New(ErrPanic, "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
}

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.toBMError()
listenerLock.Lock()
defer listenerLock.Unlock()
for _, v := range listener {
v(m, valErr)
}
}

697
exerr/meta.go Normal file
View File

@@ -0,0 +1,697 @@
package exerr
import (
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"strconv"
"strings"
"time"
)
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 {
r, err := valueFromProto(value[1:], MDTString)
if err != nil {
return err
}
v.Value = langext.Ptr(r.Value.(string))
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 valueFromProto(value string, datatype metaDataType) (MetaValue, error) {
obj := MetaValue{}
err := obj.Deserialize(value, datatype)
if err != nil {
return MetaValue{}, err
}
return obj, nil
}
func metaFromProto(proto []*spbmodels.CustomError_MetaValue) MetaMap {
r := make(MetaMap)
for _, v := range proto {
mval, err := valueFromProto(v.Value, metaDataType(v.Type))
if err != nil {
log.Warn().Err(err).Msg("metaFromProto failed for " + v.Key)
continue
}
r[v.Key] = mval
}
return r
}
func (mm MetaMap) ToProto() []*spbmodels.CustomError_MetaValue {
if mm == nil {
return make([]*spbmodels.CustomError_MetaValue, 0)
}
r := make([]*spbmodels.CustomError_MetaValue, 0, len(mm))
for k, v := range mm {
strval, err := v.SerializeValue()
if err != nil {
log.Warn().Err(err).Msg("MetaMap.ToProto failed for " + k)
continue
}
r = append(r, &spbmodels.CustomError_MetaValue{
Key: k,
Type: string(v.DataType),
Value: strval,
})
}
return r
}
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/wrapper.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}
}
}
}

View File

@@ -1,69 +0,0 @@
package ginext
import (
"bringman.de/common/shared/bmerror"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"net/http"
)
func ShouldBind(g *gin.Context, uri interface{}, query interface{}, body interface{}) error {
if uri != nil {
if err := g.ShouldBindUri(uri); err != nil {
if vErrs, ok := err.(validator.ValidationErrors); ok {
return bmerror.Wrap(vErrs, "Could not validate request parameter (uri)").
Errs("inner", convertValidationErrors(vErrs)).
WithType(bmerror.ErrQueryValidation).
WithStatuscode(http.StatusBadRequest).
Build()
} else {
return bmerror.Wrap(err, "Could not parse request parameter (uri)").
WithType(bmerror.ErrQueryParse).
WithStatuscode(http.StatusBadRequest).
Build()
}
}
}
if query != nil {
if err := g.ShouldBindQuery(query); err != nil {
if vErrs, ok := err.(validator.ValidationErrors); ok {
return bmerror.Wrap(vErrs, "Could not validate request parameter (query)").
Errs("inner", convertValidationErrors(vErrs)).
WithType(bmerror.ErrQueryValidation).
WithStatuscode(http.StatusBadRequest).
Build()
} else {
return bmerror.Wrap(err, "Could not parse request parameter (query)").
WithType(bmerror.ErrQueryParse).
WithStatuscode(http.StatusBadRequest).
Build()
}
}
}
if body != nil {
if err := g.ShouldBindJSON(body); err != nil {
if vErrs, ok := err.(validator.ValidationErrors); ok {
return bmerror.Wrap(vErrs, "Could not validate request parameter (body:json)").
Errs("inner", convertValidationErrors(vErrs)).
WithType(bmerror.ErrQueryValidation).
WithStatuscode(http.StatusBadRequest).
Build()
} else {
return bmerror.Wrap(err, "Could not parse request parameter (body:json)").
WithType(bmerror.ErrQueryParse).
WithStatuscode(http.StatusBadRequest).
Build()
}
}
}
return nil
}
func convertValidationErrors(e validator.ValidationErrors) []error {
r := make([]error, 0, len(e))
for _, v := range e {
r = append(r, v)
}
return r
}

View File

@@ -1,42 +0,0 @@
package ginext
import (
"bringman.de/common/shared/bmerror"
"context"
"github.com/gin-gonic/gin"
"net/http"
)
func NewEngine() *gin.Engine {
engine := gin.New()
engine.RedirectFixedPath = false
engine.RedirectTrailingSlash = false
engine.Use(gin.CustomRecovery(func(c *gin.Context, err interface{}) {
ctx := context.Background()
bmerror.
New(bmerror.ErrGinPanic, "gin request caused panic").
Interface("panic-object", err).
Stack().
GinReq(ctx, c, c.Request).
WithStatuscode(http.StatusInternalServerError).
Output(ctx, c)
}))
return engine
}
func NoRouteHandler() func(c *gin.Context) {
return func(g *gin.Context) {
bmerror.New(bmerror.ErrRouteNotFound, "Route not found").
Str("FullPath", g.FullPath()).
Str("Method", g.Request.Method).
Str("URL", g.Request.URL.String()).
Str("RequestURI", g.Request.RequestURI).
Str("Proto", g.Request.Proto).
Any("Header", g.Request.Header).
Output(context.Background(), g)
}
}

View File

@@ -1,3 +0,0 @@
module blackforestbytes.com/goext/gin
go 1.19

27
go.mod Normal file
View File

@@ -0,0 +1,27 @@
module gogs.mikescher.com/BlackForestBytes/goext
go 1.19
require (
golang.org/x/sys v0.5.0
golang.org/x/term v0.3.0
)
require (
github.com/golang/snappy v0.0.1 // indirect
github.com/jmoiron/sqlx v1.3.5 // indirect
github.com/klauspost/compress v1.13.6 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/rs/zerolog v1.29.0 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.1 // indirect
github.com/xdg-go/stringprep v1.0.3 // indirect
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
go.mongodb.org/mongo-driver v1.11.2 // indirect
golang.org/x/crypto v0.4.0 // indirect
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
golang.org/x/text v0.5.0 // indirect
)

78
go.sum Normal file
View File

@@ -0,0 +1,78 @@
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/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.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
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/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
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-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
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/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w=
github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.1 h1:VOMT+81stJgXW3CpHyqHN3AXDYIMsx56mEFrB37Mb/E=
github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
github.com/xdg-go/stringprep v1.0.3 h1:kdwGpVNwPFtjs98xCGkHjQtGKh86rDcRZN17QEMCOIs=
github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
go.mongodb.org/mongo-driver v1.11.2 h1:+1v2rDQUWNcGW7/7E0Jvdz51V38XXxJfhzbV17aNHCw=
go.mongodb.org/mongo-driver v1.11.2/go.mod h1:s7p5vEtfbeR1gYi6pnj3c3/urpbLv2T5Sfd6Rp2HBB8=
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/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
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/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-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-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/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.1.0 h1:g6Z6vPFA9dYBAF7DWcH6sCcOntplXsDKcliusYijMlw=
golang.org/x/term v0.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/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -1,56 +0,0 @@
package dataext
import "io"
type BufferedReadCloser interface {
io.ReadCloser
BufferedAll() ([]byte, error)
}
type bufferedReadCloser struct {
buffer []byte
inner io.ReadCloser
finished bool
}
func (b *bufferedReadCloser) Read(p []byte) (int, error) {
n, err := b.inner.Read(p)
if n > 0 {
b.buffer = append(b.buffer, p[0:n]...)
}
if err == io.EOF {
b.finished = true
}
return n, err
}
func NewBufferedReadCloser(sub io.ReadCloser) BufferedReadCloser {
return &bufferedReadCloser{
buffer: make([]byte, 0, 1024),
inner: sub,
finished: false,
}
}
func (b *bufferedReadCloser) Close() error {
err := b.inner.Close()
if err != nil {
b.finished = true
}
return err
}
func (b *bufferedReadCloser) BufferedAll() ([]byte, error) {
arr := make([]byte, 1024)
for !b.finished {
_, err := b.Read(arr)
if err != nil && err != io.EOF {
return nil, err
}
}
return b.buffer, nil
}

View File

@@ -1,3 +0,0 @@
module blackforestbytes.com/goext/lang
go 1.19

View File

@@ -1,134 +0,0 @@
package langext
import (
"reflect"
)
func ForceArray[T any](v []T) []T {
if v == nil {
return make([]T, 0)
} else {
return v
}
}
func ReverseArray[T any](v []T) {
for i, j := 0, len(v)-1; i < j; i, j = i+1, j-1 {
v[i], v[j] = v[j], v[i]
}
}
func InArray[T comparable](needle T, haystack []T) bool {
for _, v := range haystack {
if v == needle {
return true
}
}
return false
}
func ArrUnique[T comparable](array []T) []T {
m := make(map[T]bool, len(array))
for _, v := range array {
m[v] = true
}
result := make([]T, 0, len(m))
for v := range m {
result = append(result, v)
}
return result
}
func ArrEqualsExact[T comparable](arr1 []T, arr2 []T) bool {
if len(arr1) != len(arr2) {
return false
}
for i := range arr1 {
if arr1[i] != arr2[i] {
return false
}
}
return true
}
func ArrAll(arr interface{}, fn func(int) bool) bool {
av := reflect.ValueOf(arr)
for i := 0; i < av.Len(); i++ {
if !fn(i) {
return false
}
}
return true
}
func ArrAllErr(arr interface{}, fn func(int) (bool, error)) (bool, error) {
av := reflect.ValueOf(arr)
for i := 0; i < av.Len(); i++ {
v, err := fn(i)
if err != nil {
return false, err
}
if !v {
return false, nil
}
}
return true, nil
}
func ArrNone(arr interface{}, fn func(int) bool) bool {
av := reflect.ValueOf(arr)
for i := 0; i < av.Len(); i++ {
if fn(i) {
return false
}
}
return true
}
func ArrNoneErr(arr interface{}, fn func(int) (bool, error)) (bool, error) {
av := reflect.ValueOf(arr)
for i := 0; i < av.Len(); i++ {
v, err := fn(i)
if err != nil {
return false, err
}
if v {
return false, nil
}
}
return true, nil
}
func ArrAny(arr interface{}, fn func(int) bool) bool {
av := reflect.ValueOf(arr)
for i := 0; i < av.Len(); i++ {
if fn(i) {
return true
}
}
return false
}
func ArrAnyErr(arr interface{}, fn func(int) (bool, error)) (bool, error) {
av := reflect.ValueOf(arr)
for i := 0; i < av.Len(); i++ {
v, err := fn(i)
if err != nil {
return false, err
}
if v {
return true, nil
}
}
return false, nil
}
func AddToSet[T comparable](set []T, add T) []T {
for _, v := range set {
if v == add {
return set
}
}
return append(set, add)
}

View File

@@ -1,16 +0,0 @@
package langext
import "fmt"
func FormatBytesToSI(b uint64) string {
const unit = 1000
if b < unit {
return fmt.Sprintf("%d B", b)
}
div, exp := uint64(unit), 0
for n := b / unit; n >= unit; n /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "kMGTPE"[exp])
}

View File

@@ -1,13 +0,0 @@
package mathext
func AvgFloat64(arr []float64) float64 {
return SumFloat64(arr) / float64(len(arr))
}
func SumFloat64(arr []float64) float64 {
sum := 0.0
for _, v := range arr {
sum += v
}
return sum
}

View File

@@ -1,41 +0,0 @@
package mathext
func Sum(v []float64) float64 {
total := float64(0)
for _, v := range v {
total += v
}
return total
}
func Mean(v []float64) float64 {
return Sum(v) / float64(len(v))
}
func Median(v []float64) float64 {
if len(v)%2 == 1 {
return v[len(v)/2]
} else {
return (v[len(v)/2-1] + v[len(v)/2]) / float64(2)
}
}
func Min(v []float64) float64 {
r := v[0]
for _, val := range v {
if val < r {
r = val
}
}
return r
}
func Max(v []float64) float64 {
r := v[0]
for _, val := range v {
if val > r {
r = val
}
}
return r
}

View File

@@ -1,55 +0,0 @@
package timeext
import "time"
func FromSeconds(v int) time.Duration {
return time.Duration(int64(v) * int64(time.Second))
}
func FromSecondsInt32(v int32) time.Duration {
return time.Duration(int64(v) * int64(time.Second))
}
func FromSecondsInt64(v int64) time.Duration {
return time.Duration(v * int64(time.Second))
}
func FromSecondsFloat32(v float32) time.Duration {
return time.Duration(int64(v * float32(time.Second)))
}
func FromSecondsFloat64(v float64) time.Duration {
return time.Duration(int64(v * float64(time.Second)))
}
func FromSecondsFloat(v float64) time.Duration {
return time.Duration(int64(v * float64(time.Second)))
}
func FromMinutes(v int) time.Duration {
return time.Duration(int64(v) * int64(time.Minute))
}
func FromMinutesFloat(v float64) time.Duration {
return time.Duration(int64(v * float64(time.Minute)))
}
func FromMinutesFloat64(v float64) time.Duration {
return time.Duration(int64(v * float64(time.Minute)))
}
func FromHoursFloat64(v float64) time.Duration {
return time.Duration(int64(v * float64(time.Hour)))
}
func FromDays(v int) time.Duration {
return time.Duration(int64(v) * int64(24) * int64(time.Hour))
}
func FromMilliseconds(v int) time.Duration {
return time.Duration(int64(v) * int64(time.Millisecond))
}
func FromMillisecondsFloat(v float64) time.Duration {
return time.Duration(int64(v * float64(time.Millisecond)))
}

292
langext/array.go Normal file
View File

@@ -0,0 +1,292 @@
package langext
import (
"reflect"
)
func BoolCount(arr ...bool) int {
c := 0
for _, v := range arr {
if v {
c++
}
}
return c
}
func Range[T IntegerConstraint](start T, end T) []T {
r := make([]T, 0, end-start)
for i := start; i < end; i++ {
r = append(r, i)
}
return r
}
func ForceArray[T any](v []T) []T {
if v == nil {
return make([]T, 0)
} else {
return v
}
}
func ReverseArray[T any](v []T) {
for i, j := 0, len(v)-1; i < j; i, j = i+1, j-1 {
v[i], v[j] = v[j], v[i]
}
}
func InArray[T comparable](needle T, haystack []T) bool {
for _, v := range haystack {
if v == needle {
return true
}
}
return false
}
func ArrUnique[T comparable](array []T) []T {
m := make(map[T]bool, len(array))
for _, v := range array {
m[v] = true
}
result := make([]T, 0, len(m))
for v := range m {
result = append(result, v)
}
return result
}
func ArrEqualsExact[T comparable](arr1 []T, arr2 []T) bool {
if len(arr1) != len(arr2) {
return false
}
for i := range arr1 {
if arr1[i] != arr2[i] {
return false
}
}
return true
}
func ArrAll[T any](arr []T, fn func(T) bool) bool {
for _, av := range arr {
if !fn(av) {
return false
}
}
return true
}
func ArrAllErr[T any](arr []T, fn func(T) (bool, error)) (bool, error) {
for _, av := range arr {
v, err := fn(av)
if err != nil {
return false, err
}
if !v {
return false, nil
}
}
return true, nil
}
func ArrNone[T any](arr []T, fn func(T) bool) bool {
for _, av := range arr {
if fn(av) {
return false
}
}
return true
}
func ArrNoneErr[T any](arr []T, fn func(T) (bool, error)) (bool, error) {
for _, av := range arr {
v, err := fn(av)
if err != nil {
return false, err
}
if v {
return false, nil
}
}
return true, nil
}
func ArrAny[T any](arr []T, fn func(T) bool) bool {
for _, av := range arr {
if fn(av) {
return true
}
}
return false
}
func ArrAnyErr[T any](arr []T, fn func(T) (bool, error)) (bool, error) {
for _, av := range arr {
v, err := fn(av)
if err != nil {
return false, err
}
if v {
return true, nil
}
}
return false, nil
}
func ArrIdxAll(arr any, fn func(int) bool) bool {
av := reflect.ValueOf(arr)
for i := 0; i < av.Len(); i++ {
if !fn(i) {
return false
}
}
return true
}
func ArrIdxAllErr(arr any, fn func(int) (bool, error)) (bool, error) {
av := reflect.ValueOf(arr)
for i := 0; i < av.Len(); i++ {
v, err := fn(i)
if err != nil {
return false, err
}
if !v {
return false, nil
}
}
return true, nil
}
func ArrIdxNone(arr any, fn func(int) bool) bool {
av := reflect.ValueOf(arr)
for i := 0; i < av.Len(); i++ {
if fn(i) {
return false
}
}
return true
}
func ArrIdxNoneErr(arr any, fn func(int) (bool, error)) (bool, error) {
av := reflect.ValueOf(arr)
for i := 0; i < av.Len(); i++ {
v, err := fn(i)
if err != nil {
return false, err
}
if v {
return false, nil
}
}
return true, nil
}
func ArrIdxAny(arr any, fn func(int) bool) bool {
av := reflect.ValueOf(arr)
for i := 0; i < av.Len(); i++ {
if fn(i) {
return true
}
}
return false
}
func ArrIdxAnyErr(arr any, fn func(int) (bool, error)) (bool, error) {
av := reflect.ValueOf(arr)
for i := 0; i < av.Len(); i++ {
v, err := fn(i)
if err != nil {
return false, err
}
if v {
return true, nil
}
}
return false, nil
}
func ArrFirst[T any](arr []T, comp func(v T) bool) (T, bool) {
for _, v := range arr {
if comp(v) {
return v, true
}
}
return *new(T), false
}
func ArrLast[T any](arr []T, comp func(v T) bool) (T, bool) {
found := false
result := *new(T)
for _, v := range arr {
if comp(v) {
found = true
result = v
}
}
return result, found
}
func ArrFirstIndex[T comparable](arr []T, needle T) int {
for i, v := range arr {
if v == needle {
return i
}
}
return -1
}
func ArrLastIndex[T comparable](arr []T, needle T) int {
result := -1
for i, v := range arr {
if v == needle {
result = i
}
}
return result
}
func AddToSet[T comparable](set []T, add T) []T {
for _, v := range set {
if v == add {
return set
}
}
return append(set, add)
}
func ArrMap[T1 any, T2 any](arr []T1, conv func(v T1) T2) []T2 {
r := make([]T2, len(arr))
for i, v := range arr {
r[i] = conv(v)
}
return r
}
func ArrFilterMap[T1 any, T2 any](arr []T1, filter func(v T1) bool, conv func(v T1) T2) []T2 {
r := make([]T2, 0, len(arr))
for _, v := range arr {
if filter(v) {
r = append(r, conv(v))
}
}
return r
}
func ArrSum[T NumberConstraint](arr []T) T {
var r T = 0
for _, v := range arr {
r += v
}
return r
}
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
}

74
langext/base62.go Normal file
View File

@@ -0,0 +1,74 @@
package langext
import (
"crypto/rand"
"errors"
"math"
"math/big"
"strings"
)
var (
base62Base = uint64(62)
base62CharacterSet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
)
func RandBase62(rlen int) string {
bi52 := big.NewInt(int64(len(base62CharacterSet)))
randMax := big.NewInt(math.MaxInt64)
r := ""
for i := 0; i < rlen; i++ {
v, err := rand.Int(rand.Reader, randMax)
if err != nil {
panic(err)
}
r += string(base62CharacterSet[v.Mod(v, bi52).Int64()])
}
return r
}
func EncodeBase62(num uint64) string {
if num == 0 {
return "0"
}
b := make([]byte, 0)
// loop as long the num is bigger than zero
for num > 0 {
r := num % base62Base
num -= r
num /= base62Base
b = append([]byte{base62CharacterSet[int(r)]}, b...)
}
return string(b)
}
func DecodeBase62(str string) (uint64, error) {
if str == "" {
return 0, errors.New("empty string")
}
result := uint64(0)
for _, v := range str {
result *= base62Base
pos := strings.IndexRune(base62CharacterSet, v)
if pos == -1 {
return 0, errors.New("invalid character: " + string(v))
}
result += uint64(pos)
}
return result, nil
}

49
langext/bool.go Normal file
View File

@@ -0,0 +1,49 @@
package langext
func FormatBool(v bool, strTrue string, strFalse string) string {
if v {
return strTrue
} else {
return strFalse
}
}
func Conditional[T any](v bool, resTrue T, resFalse T) T {
if v {
return resTrue
} else {
return resFalse
}
}
func ConditionalFn00[T any](v bool, resTrue T, resFalse T) T {
if v {
return resTrue
} else {
return resFalse
}
}
func ConditionalFn10[T any](v bool, resTrue func() T, resFalse T) T {
if v {
return resTrue()
} else {
return resFalse
}
}
func ConditionalFn01[T any](v bool, resTrue T, resFalse func() T) T {
if v {
return resTrue
} else {
return resFalse()
}
}
func ConditionalFn11[T any](v bool, resTrue func() T, resFalse func() T) T {
if v {
return resTrue()
} else {
return resFalse()
}
}

46
langext/bytes.go Normal file
View File

@@ -0,0 +1,46 @@
package langext
import (
"errors"
"fmt"
)
func FormatBytesToSI(b uint64) string {
const unit = 1000
if b < unit {
return fmt.Sprintf("%d B", b)
}
div, exp := uint64(unit), 0
for n := b / unit; n >= unit; n /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "kMGTPE"[exp])
}
func BytesXOR(a []byte, b []byte) ([]byte, error) {
if len(a) != len(b) {
return nil, errors.New("length mismatch")
}
r := make([]byte, len(a))
for i := 0; i < len(a); i++ {
r[i] = a[i] ^ b[i]
}
return r, nil
}
func FormatBytes(b int64) string {
const unit = 1024
if b < unit {
return fmt.Sprintf("%d B", b)
}
div, exp := int64(unit), 0
for n := b / unit; n >= unit; n /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.1f %ciB", float64(b)/float64(div), "KMGTPE"[exp])
}

View File

@@ -30,3 +30,34 @@ func CompareIntArr(arr1 []int, arr2 []int) bool {
return false
}
func CompareArr[T OrderedConstraint](arr1 []T, arr2 []T) bool {
for i := 0; i < len(arr1) || i < len(arr2); i++ {
if i < len(arr1) && i < len(arr2) {
if arr1[i] < arr2[i] {
return true
} else if arr1[i] > arr2[i] {
return false
} else {
continue
}
}
if i < len(arr1) {
return true
} else { // if i < len(arr2)
return false
}
}
return false
}

21
langext/coords.go Normal file
View File

@@ -0,0 +1,21 @@
package langext
import "math"
func DegToRad(deg float64) float64 {
return deg * (math.Pi / 180.0)
}
func RadToDeg(rad float64) float64 {
return rad / (math.Pi * 180.0)
}
func GeoDistance(lon1 float64, lat1 float64, lon2 float64, lat2 float64) float64 {
var d1 = DegToRad(lat1)
var num1 = DegToRad(lon1)
var d2 = DegToRad(lat2)
var num2 = DegToRad(lon2) - num1
var d3 = math.Pow(math.Sin((d2-d1)/2.0), 2.0) + math.Cos(d1)*math.Cos(d2)*math.Pow(math.Sin(num2/2.0), 2.0)
return 6376500.0 * (2.0 * math.Atan2(math.Sqrt(d3), math.Sqrt(1.0-d3)))
}

33
langext/generics.go Normal file
View File

@@ -0,0 +1,33 @@
package langext
type IntConstraint interface {
int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64
}
type SignedConstraint interface {
~int | ~int8 | ~int16 | ~int32 | ~int64
}
type UnsignedConstraint interface {
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
}
type IntegerConstraint interface {
SignedConstraint | UnsignedConstraint
}
type FloatConstraint interface {
~float32 | ~float64
}
type ComplexConstraint interface {
~complex64 | ~complex128
}
type OrderedConstraint interface {
IntegerConstraint | FloatConstraint | ~string
}
type NumberConstraint interface {
IntegerConstraint | FloatConstraint
}

65
langext/json.go Normal file
View File

@@ -0,0 +1,65 @@
package langext
import (
"bytes"
"encoding/json"
"fmt"
)
type H map[string]any
type A []any
func TryPrettyPrintJson(str string) string {
var prettyJSON bytes.Buffer
if err := json.Indent(&prettyJSON, []byte(str), "", " "); err != nil {
return str
}
return prettyJSON.String()
}
func PrettyPrintJson(str string) (string, bool) {
var prettyJSON bytes.Buffer
if err := json.Indent(&prettyJSON, []byte(str), "", " "); err != nil {
return str, false
}
return prettyJSON.String(), true
}
func PatchJson[JV string | []byte](rawjson JV, key string, value any) (JV, error) {
var err error
var jsonpayload map[string]any
err = json.Unmarshal([]byte(rawjson), &jsonpayload)
if err != nil {
return *new(JV), fmt.Errorf("failed to unmarshal payload: %w", err)
}
jsonpayload[key] = value
newjson, err := json.Marshal(jsonpayload)
if err != nil {
return *new(JV), fmt.Errorf("failed to re-marshal payload: %w", err)
}
return JV(newjson), nil
}
func PatchRemJson[JV string | []byte](rawjson JV, key string) (JV, error) {
var err error
var jsonpayload map[string]any
err = json.Unmarshal([]byte(rawjson), &jsonpayload)
if err != nil {
return *new(JV), fmt.Errorf("failed to unmarshal payload: %w", err)
}
delete(jsonpayload, key)
newjson, err := json.Marshal(jsonpayload)
if err != nil {
return *new(JV), fmt.Errorf("failed to re-marshal payload: %w", err)
}
return JV(newjson), nil
}

17
langext/maps.go Normal file
View File

@@ -0,0 +1,17 @@
package langext
func MapKeyArr[T comparable, V any](v map[T]V) []T {
result := make([]T, 0, len(v))
for k := range v {
result = append(result, k)
}
return result
}
func ArrToMap[T comparable, V any](a []V, keyfunc func(V) T) map[T]V {
result := make(map[T]V, len(a))
for _, v := range a {
result[keyfunc(v)] = v
}
return result
}

11
langext/os.go Normal file
View File

@@ -0,0 +1,11 @@
package langext
import "os"
func FileExists(filename string) bool {
info, err := os.Stat(filename)
if os.IsNotExist(err) {
return false
}
return !info.IsDir()
}

View File

@@ -34,3 +34,7 @@ func IsNil(i interface{}) bool {
}
return false
}
func PtrEquals[T comparable](v1 *T, v2 *T) bool {
return (v1 == nil && v2 == nil) || (v1 != nil && v2 != nil && *v1 == *v2)
}

15
langext/rand.go Normal file
View File

@@ -0,0 +1,15 @@
package langext
import (
"crypto/rand"
"io"
)
func RandBytes(size int) []byte {
b := make([]byte, size)
_, err := io.ReadFull(rand.Reader, b)
if err != nil {
panic(err)
}
return b
}

57
langext/sort.go Normal file
View File

@@ -0,0 +1,57 @@
package langext
import "sort"
func Sort[T OrderedConstraint](arr []T) {
sort.Slice(arr, func(i1, i2 int) bool {
return arr[i1] < arr[i2]
})
}
func SortStable[T OrderedConstraint](arr []T) {
sort.SliceStable(arr, func(i1, i2 int) bool {
return arr[i1] < arr[i2]
})
}
func IsSorted[T OrderedConstraint](arr []T) bool {
return sort.SliceIsSorted(arr, func(i1, i2 int) bool {
return arr[i1] < arr[i2]
})
}
func SortSlice[T any](arr []T, less func(v1, v2 T) bool) {
sort.Slice(arr, func(i1, i2 int) bool {
return less(arr[i1], arr[i2])
})
}
func SortSliceStable[T any](arr []T, less func(v1, v2 T) bool) {
sort.SliceStable(arr, func(i1, i2 int) bool {
return less(arr[i1], arr[i2])
})
}
func IsSliceSorted[T any](arr []T, less func(v1, v2 T) bool) bool {
return sort.SliceIsSorted(arr, func(i1, i2 int) bool {
return less(arr[i1], arr[i2])
})
}
func SortBy[TElem any, TSel OrderedConstraint](arr []TElem, selector func(v TElem) TSel) {
sort.Slice(arr, func(i1, i2 int) bool {
return selector(arr[i1]) < selector(arr[i2])
})
}
func SortByStable[TElem any, TSel OrderedConstraint](arr []TElem, selector func(v TElem) TSel) {
sort.SliceStable(arr, func(i1, i2 int) bool {
return selector(arr[i1]) < selector(arr[i2])
})
}
func IsSortedBy[TElem any, TSel OrderedConstraint](arr []TElem, selector func(v TElem) TSel) {
sort.SliceStable(arr, func(i1, i2 int) bool {
return selector(arr[i1]) < selector(arr[i2])
})
}

View File

@@ -61,3 +61,57 @@ func ConvertStringerArray[T fmt.Stringer](inarr []T) []string {
}
return result
}
func StrRunePadLeft(str string, pad string, padlen int) string {
if pad == "" {
pad = " "
}
if len([]rune(str)) >= padlen {
return str
}
return strings.Repeat(pad, padlen-len([]rune(str)))[0:(padlen-len([]rune(str)))] + str
}
func StrRunePadRight(str string, pad string, padlen int) string {
if pad == "" {
pad = " "
}
if len([]rune(str)) >= padlen {
return str
}
return str + strings.Repeat(pad, padlen-len([]rune(str)))[0:(padlen-len([]rune(str)))]
}
func Indent(str string, pad string) string {
eonl := strings.HasSuffix(str, "\n")
r := ""
for _, v := range strings.Split(str, "\n") {
r += pad + v + "\n"
}
if eonl {
r = r[0 : len(r)-1]
}
return r
}
func NumToStringOpt[V IntConstraint](v *V, fallback string) string {
if v == nil {
return fallback
} else {
return fmt.Sprintf("%d", v)
}
}
func StrRepeat(val string, count int) string {
r := ""
for i := 0; i < count; i++ {
r += val
}
return r
}

134
langext/uuid.go Normal file
View File

@@ -0,0 +1,134 @@
package langext
import (
"crypto/rand"
"encoding/hex"
"io"
"strings"
)
func NewUUID() ([16]byte, error) {
var uuid [16]byte
_, err := io.ReadFull(rand.Reader, uuid[:])
if err != nil {
return [16]byte{}, err
}
uuid[6] = (uuid[6] & 0x0f) | 0x40 // Version 4
uuid[8] = (uuid[8] & 0x3f) | 0x80 // Variant is 10
return uuid, nil
}
func NewHexUUID() (string, error) {
uuid, err := NewUUID()
if err != nil {
return "", err
}
// Result: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
var dst = make([]byte, 36)
hex.Encode(dst, uuid[:4])
dst[8] = '-'
hex.Encode(dst[9:13], uuid[4:6])
dst[13] = '-'
hex.Encode(dst[14:18], uuid[6:8])
dst[18] = '-'
hex.Encode(dst[19:23], uuid[8:10])
dst[23] = '-'
hex.Encode(dst[24:], uuid[10:])
return string(dst), nil
}
func NewUpperHexUUID() (string, error) {
uuid, err := NewUUID()
if err != nil {
return "", err
}
// Result: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
var dst = make([]byte, 36)
hex.Encode(dst, uuid[:4])
dst[8] = '-'
hex.Encode(dst[9:13], uuid[4:6])
dst[13] = '-'
hex.Encode(dst[14:18], uuid[6:8])
dst[18] = '-'
hex.Encode(dst[19:23], uuid[8:10])
dst[23] = '-'
hex.Encode(dst[24:], uuid[10:])
return strings.ToUpper(string(dst)), nil
}
func NewRawHexUUID() (string, error) {
uuid, err := NewUUID()
if err != nil {
return "", err
}
// Result: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
var dst = make([]byte, 32)
hex.Encode(dst, uuid[:4])
hex.Encode(dst[8:12], uuid[4:6])
hex.Encode(dst[12:16], uuid[6:8])
hex.Encode(dst[16:20], uuid[8:10])
hex.Encode(dst[20:], uuid[10:])
return strings.ToUpper(string(dst)), nil
}
func NewBracesUUID() (string, error) {
uuid, err := NewUUID()
if err != nil {
return "", err
}
// Result: {XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}
var dst = make([]byte, 38)
dst[0] = '{'
hex.Encode(dst, uuid[:5])
dst[9] = '-'
hex.Encode(dst[10:14], uuid[4:6])
dst[14] = '-'
hex.Encode(dst[15:19], uuid[6:8])
dst[19] = '-'
hex.Encode(dst[20:24], uuid[8:10])
dst[24] = '-'
hex.Encode(dst[25:], uuid[10:])
dst[37] = '}'
return strings.ToUpper(string(dst)), nil
}
func NewParensUUID() (string, error) {
uuid, err := NewUUID()
if err != nil {
return "", err
}
// Result: (XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX)
var dst = make([]byte, 38)
dst[0] = '('
hex.Encode(dst, uuid[:5])
dst[9] = '-'
hex.Encode(dst[10:14], uuid[4:6])
dst[14] = '-'
hex.Encode(dst[15:19], uuid[6:8])
dst[19] = '-'
hex.Encode(dst[20:24], uuid[8:10])
dst[24] = '-'
hex.Encode(dst[25:], uuid[10:])
dst[37] = ')'
return strings.ToUpper(string(dst)), nil
}

49
mathext/math.go Normal file
View File

@@ -0,0 +1,49 @@
package mathext
import "gogs.mikescher.com/BlackForestBytes/goext/langext"
func AvgFloat64(arr []float64) float64 {
return SumFloat64(arr) / float64(len(arr))
}
func SumFloat64(arr []float64) float64 {
sum := 0.0
for _, v := range arr {
sum += v
}
return sum
}
func Max[T langext.OrderedConstraint](v1 T, v2 T) T {
if v1 > v2 {
return v1
} else {
return v2
}
}
func Min[T langext.OrderedConstraint](v1 T, v2 T) T {
if v1 < v2 {
return v1
} else {
return v2
}
}
func Abs[T langext.NumberConstraint](v T) T {
if v < 0 {
return -v
} else {
return v
}
}
func Clamp[T langext.NumberConstraint](v T, min T, max T) T {
if v < min {
return min
} else if v > max {
return max
} else {
return v
}
}

43
mathext/statistics.go Normal file
View File

@@ -0,0 +1,43 @@
package mathext
import "gogs.mikescher.com/BlackForestBytes/goext/langext"
func Sum[T langext.NumberConstraint](v []T) T {
total := T(0)
for _, v := range v {
total += v
}
return total
}
func Mean[T langext.FloatConstraint](v []T) T {
return Sum(v) / T(len(v))
}
func Median[T langext.FloatConstraint](v []T) T {
if len(v)%2 == 1 {
return v[len(v)/2]
} else {
return (v[len(v)/2-1] + v[len(v)/2]) / T(2)
}
}
func ArrMin[T langext.OrderedConstraint](v []T) T {
r := v[0]
for _, val := range v {
if val < r {
r = val
}
}
return r
}
func ArrMax[T langext.OrderedConstraint](v []T) T {
r := v[0]
for _, val := range v {
if val > r {
r = val
}
}
return r
}

View File

@@ -1,6 +0,0 @@
module blackforestbytes.com/goext/mongo
require (
go.mongodb.org/mongo-driver v1.5.3
)
go 1.19

View File

130
rext/wrapper.go Normal file
View File

@@ -0,0 +1,130 @@
package rext
import "regexp"
type Regex interface {
IsMatch(haystack string) bool
MatchFirst(haystack string) (RegexMatch, bool)
MatchAll(haystack string) []RegexMatch
ReplaceAll(haystack string, repl string, literal bool) string
ReplaceAllFunc(haystack string, repl func(string) string) string
RemoveAll(haystack string) string
GroupCount() int
}
type regexWrapper struct {
rex *regexp.Regexp
subnames []string
}
type RegexMatch struct {
haystack string
submatchesIndex []int
subnames []string
}
type RegexMatchGroup struct {
haystack string
start int
end int
}
func W(rex *regexp.Regexp) Regex {
return &regexWrapper{rex: rex, subnames: rex.SubexpNames()}
}
// ---------------------------------------------------------------------------------------------------------------------
func (w *regexWrapper) IsMatch(haystack string) bool {
return w.rex.MatchString(haystack)
}
func (w *regexWrapper) MatchFirst(haystack string) (RegexMatch, bool) {
res := w.rex.FindStringSubmatchIndex(haystack)
if res == nil {
return RegexMatch{}, false
}
return RegexMatch{haystack: haystack, submatchesIndex: res, subnames: w.subnames}, true
}
func (w *regexWrapper) MatchAll(haystack string) []RegexMatch {
resarr := w.rex.FindAllStringSubmatchIndex(haystack, -1)
matches := make([]RegexMatch, 0, len(resarr))
for _, res := range resarr {
matches = append(matches, RegexMatch{haystack: haystack, submatchesIndex: res, subnames: w.subnames})
}
return matches
}
func (w *regexWrapper) ReplaceAll(haystack string, repl string, literal bool) string {
if literal {
// do not expand placeholder aka $1, $2, ...
return w.rex.ReplaceAllLiteralString(haystack, repl)
} else {
return w.rex.ReplaceAllString(haystack, repl)
}
}
func (w *regexWrapper) ReplaceAllFunc(haystack string, repl func(string) string) string {
return w.rex.ReplaceAllStringFunc(haystack, repl)
}
func (w *regexWrapper) RemoveAll(haystack string) string {
return w.rex.ReplaceAllLiteralString(haystack, "")
}
// GroupCount returns the amount of groups in this match, does not count group-0 (whole match)
func (w *regexWrapper) GroupCount() int {
return len(w.subnames) - 1
}
// ---------------------------------------------------------------------------------------------------------------------
func (m RegexMatch) FullMatch() RegexMatchGroup {
return RegexMatchGroup{haystack: m.haystack, start: m.submatchesIndex[0], end: m.submatchesIndex[1]}
}
// GroupCount returns the amount of groups in this match, does not count group-0 (whole match)
func (m RegexMatch) GroupCount() int {
return len(m.subnames) - 1
}
// GroupByIndex returns the value of a matched group (group 0 == whole match)
func (m RegexMatch) GroupByIndex(idx int) RegexMatchGroup {
return RegexMatchGroup{haystack: m.haystack, start: m.submatchesIndex[idx*2], end: m.submatchesIndex[idx*2+1]}
}
// GroupByName returns the value of a matched group (group 0 == whole match)
func (m RegexMatch) GroupByName(name string) RegexMatchGroup {
for idx, subname := range m.subnames {
if subname == name {
return RegexMatchGroup{haystack: m.haystack, start: m.submatchesIndex[idx*2], end: m.submatchesIndex[idx*2+1]}
}
}
panic("failed to find regex-group by name")
}
// ---------------------------------------------------------------------------------------------------------------------
func (g RegexMatchGroup) Value() string {
return g.haystack[g.start:g.end]
}
func (g RegexMatchGroup) Start() int {
return g.start
}
func (g RegexMatchGroup) End() int {
return g.end
}
func (g RegexMatchGroup) Range() (int, int) {
return g.start, g.end
}
func (g RegexMatchGroup) Length() int {
return g.end - g.start
}

45
rfctime/interface.go Normal file
View File

@@ -0,0 +1,45 @@
package rfctime
import "time"
type RFCTime interface {
Time() time.Time
Serialize() string
UnmarshalJSON(bytes []byte) error
MarshalJSON() ([]byte, error)
MarshalBinary() ([]byte, error)
UnmarshalBinary(data []byte) error
GobEncode() ([]byte, error)
GobDecode(data []byte) error
MarshalText() ([]byte, error)
UnmarshalText(data []byte) error
After(u RFCTime) bool
Before(u RFCTime) bool
Equal(u RFCTime) bool
IsZero() bool
Date() (year int, month time.Month, day int)
Year() int
Month() time.Month
Day() int
Weekday() time.Weekday
ISOWeek() (year, week int)
Clock() (hour, min, sec int)
Hour() int
Minute() int
Second() int
Nanosecond() int
YearDay() int
Sub(u RFCTime) time.Duration
Unix() int64
UnixMilli() int64
UnixMicro() int64
UnixNano() int64
Format(layout string) string
GoString() string
String() string
}

182
rfctime/rfc3339.go Normal file
View File

@@ -0,0 +1,182 @@
package rfctime
import (
"encoding/json"
"time"
)
type RFC3339Time time.Time
func (t RFC3339Time) Time() time.Time {
return time.Time(t)
}
func (t RFC3339Time) MarshalBinary() ([]byte, error) {
return (time.Time)(t).MarshalBinary()
}
func (t *RFC3339Time) UnmarshalBinary(data []byte) error {
return (*time.Time)(t).UnmarshalBinary(data)
}
func (t RFC3339Time) GobEncode() ([]byte, error) {
return (time.Time)(t).GobEncode()
}
func (t *RFC3339Time) GobDecode(data []byte) error {
return (*time.Time)(t).GobDecode(data)
}
func (t *RFC3339Time) 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 = RFC3339Time(t0)
return nil
}
func (t RFC3339Time) MarshalJSON() ([]byte, error) {
str := t.Time().Format(t.FormatStr())
return json.Marshal(str)
}
func (t RFC3339Time) MarshalText() ([]byte, error) {
b := make([]byte, 0, len(t.FormatStr()))
return t.Time().AppendFormat(b, t.FormatStr()), nil
}
func (t *RFC3339Time) UnmarshalText(data []byte) error {
var err error
v, err := time.Parse(t.FormatStr(), string(data))
if err != nil {
return err
}
tt := RFC3339Time(v)
*t = tt
return nil
}
func (t RFC3339Time) Serialize() string {
return t.Time().Format(t.FormatStr())
}
func (t RFC3339Time) FormatStr() string {
return time.RFC3339
}
func (t RFC3339Time) After(u RFCTime) bool {
return t.Time().After(u.Time())
}
func (t RFC3339Time) Before(u RFCTime) bool {
return t.Time().Before(u.Time())
}
func (t RFC3339Time) Equal(u RFCTime) bool {
return t.Time().Equal(u.Time())
}
func (t RFC3339Time) IsZero() bool {
return t.Time().IsZero()
}
func (t RFC3339Time) Date() (year int, month time.Month, day int) {
return t.Time().Date()
}
func (t RFC3339Time) Year() int {
return t.Time().Year()
}
func (t RFC3339Time) Month() time.Month {
return t.Time().Month()
}
func (t RFC3339Time) Day() int {
return t.Time().Day()
}
func (t RFC3339Time) Weekday() time.Weekday {
return t.Time().Weekday()
}
func (t RFC3339Time) ISOWeek() (year, week int) {
return t.Time().ISOWeek()
}
func (t RFC3339Time) Clock() (hour, min, sec int) {
return t.Time().Clock()
}
func (t RFC3339Time) Hour() int {
return t.Time().Hour()
}
func (t RFC3339Time) Minute() int {
return t.Time().Minute()
}
func (t RFC3339Time) Second() int {
return t.Time().Second()
}
func (t RFC3339Time) Nanosecond() int {
return t.Time().Nanosecond()
}
func (t RFC3339Time) YearDay() int {
return t.Time().YearDay()
}
func (t RFC3339Time) Add(d time.Duration) RFC3339Time {
return RFC3339Time(t.Time().Add(d))
}
func (t RFC3339Time) Sub(u RFCTime) time.Duration {
return t.Time().Sub(u.Time())
}
func (t RFC3339Time) AddDate(years int, months int, days int) RFC3339Time {
return RFC3339Time(t.Time().AddDate(years, months, days))
}
func (t RFC3339Time) Unix() int64 {
return t.Time().Unix()
}
func (t RFC3339Time) UnixMilli() int64 {
return t.Time().UnixMilli()
}
func (t RFC3339Time) UnixMicro() int64 {
return t.Time().UnixMicro()
}
func (t RFC3339Time) UnixNano() int64 {
return t.Time().UnixNano()
}
func (t RFC3339Time) Format(layout string) string {
return t.Time().Format(layout)
}
func (t RFC3339Time) GoString() string {
return t.Time().GoString()
}
func (t RFC3339Time) String() string {
return t.Time().String()
}
func NewRFC3339(t time.Time) RFC3339Time {
return RFC3339Time(t)
}
func NowRFC3339() RFC3339Time {
return RFC3339Time(time.Now())
}

182
rfctime/rfc3339Nano.go Normal file
View File

@@ -0,0 +1,182 @@
package rfctime
import (
"encoding/json"
"time"
)
type RFC3339NanoTime time.Time
func (t RFC3339NanoTime) Time() time.Time {
return time.Time(t)
}
func (t RFC3339NanoTime) MarshalBinary() ([]byte, error) {
return (time.Time)(t).MarshalBinary()
}
func (t *RFC3339NanoTime) UnmarshalBinary(data []byte) error {
return (*time.Time)(t).UnmarshalBinary(data)
}
func (t RFC3339NanoTime) GobEncode() ([]byte, error) {
return (time.Time)(t).GobEncode()
}
func (t *RFC3339NanoTime) GobDecode(data []byte) error {
return (*time.Time)(t).GobDecode(data)
}
func (t *RFC3339NanoTime) 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 = RFC3339NanoTime(t0)
return nil
}
func (t RFC3339NanoTime) MarshalJSON() ([]byte, error) {
str := t.Time().Format(t.FormatStr())
return json.Marshal(str)
}
func (t RFC3339NanoTime) MarshalText() ([]byte, error) {
b := make([]byte, 0, len(t.FormatStr()))
return t.Time().AppendFormat(b, t.FormatStr()), nil
}
func (t *RFC3339NanoTime) UnmarshalText(data []byte) error {
var err error
v, err := time.Parse(t.FormatStr(), string(data))
if err != nil {
return err
}
tt := RFC3339NanoTime(v)
*t = tt
return nil
}
func (t RFC3339NanoTime) Serialize() string {
return t.Time().Format(t.FormatStr())
}
func (t RFC3339NanoTime) FormatStr() string {
return time.RFC3339Nano
}
func (t RFC3339NanoTime) After(u RFCTime) bool {
return t.Time().After(u.Time())
}
func (t RFC3339NanoTime) Before(u RFCTime) bool {
return t.Time().Before(u.Time())
}
func (t RFC3339NanoTime) Equal(u RFCTime) bool {
return t.Time().Equal(u.Time())
}
func (t RFC3339NanoTime) IsZero() bool {
return t.Time().IsZero()
}
func (t RFC3339NanoTime) Date() (year int, month time.Month, day int) {
return t.Time().Date()
}
func (t RFC3339NanoTime) Year() int {
return t.Time().Year()
}
func (t RFC3339NanoTime) Month() time.Month {
return t.Time().Month()
}
func (t RFC3339NanoTime) Day() int {
return t.Time().Day()
}
func (t RFC3339NanoTime) Weekday() time.Weekday {
return t.Time().Weekday()
}
func (t RFC3339NanoTime) ISOWeek() (year, week int) {
return t.Time().ISOWeek()
}
func (t RFC3339NanoTime) Clock() (hour, min, sec int) {
return t.Time().Clock()
}
func (t RFC3339NanoTime) Hour() int {
return t.Time().Hour()
}
func (t RFC3339NanoTime) Minute() int {
return t.Time().Minute()
}
func (t RFC3339NanoTime) Second() int {
return t.Time().Second()
}
func (t RFC3339NanoTime) Nanosecond() int {
return t.Time().Nanosecond()
}
func (t RFC3339NanoTime) YearDay() int {
return t.Time().YearDay()
}
func (t RFC3339NanoTime) Add(d time.Duration) RFC3339NanoTime {
return RFC3339NanoTime(t.Time().Add(d))
}
func (t RFC3339NanoTime) Sub(u RFCTime) time.Duration {
return t.Time().Sub(u.Time())
}
func (t RFC3339NanoTime) AddDate(years int, months int, days int) RFC3339NanoTime {
return RFC3339NanoTime(t.Time().AddDate(years, months, days))
}
func (t RFC3339NanoTime) Unix() int64 {
return t.Time().Unix()
}
func (t RFC3339NanoTime) UnixMilli() int64 {
return t.Time().UnixMilli()
}
func (t RFC3339NanoTime) UnixMicro() int64 {
return t.Time().UnixMicro()
}
func (t RFC3339NanoTime) UnixNano() int64 {
return t.Time().UnixNano()
}
func (t RFC3339NanoTime) Format(layout string) string {
return t.Time().Format(layout)
}
func (t RFC3339NanoTime) GoString() string {
return t.Time().GoString()
}
func (t RFC3339NanoTime) String() string {
return t.Time().String()
}
func NewRFC3339Nano(t time.Time) RFC3339NanoTime {
return RFC3339NanoTime(t)
}
func NowRFC3339Nano() RFC3339NanoTime {
return RFC3339NanoTime(time.Now())
}

View File

@@ -0,0 +1,51 @@
package rfctime
import (
"encoding/json"
"testing"
"time"
)
func TestRoundtrip(t *testing.T) {
type Wrap struct {
Value RFC3339NanoTime `json:"v"`
}
val1 := NewRFC3339Nano(time.Now())
w1 := Wrap{val1}
jstr1, err := json.Marshal(w1)
if err != nil {
panic(err)
}
if string(jstr1) != "{\"v\":\"2023-01-29T20:32:36.149692117+01:00\"}" {
t.Errorf("repr differs")
}
w2 := Wrap{}
err = json.Unmarshal(jstr1, &w2)
if err != nil {
panic(err)
}
jstr2, err := json.Marshal(w2)
if err != nil {
panic(err)
}
assertEqual(t, string(jstr1), string(jstr2))
if !w1.Value.Equal(&w2.Value) {
t.Errorf("time differs")
}
}
func assertEqual(t *testing.T, actual string, expected string) {
if actual != expected {
t.Errorf("values differ: Actual: '%v', Expected: '%v'", actual, expected)
}
}

176
rfctime/unix.go Normal file
View File

@@ -0,0 +1,176 @@
package rfctime
import (
"encoding/json"
"strconv"
"time"
)
type UnixTime time.Time
func (t UnixTime) Time() time.Time {
return time.Time(t)
}
func (t UnixTime) MarshalBinary() ([]byte, error) {
return (time.Time)(t).MarshalBinary()
}
func (t *UnixTime) UnmarshalBinary(data []byte) error {
return (*time.Time)(t).UnmarshalBinary(data)
}
func (t UnixTime) GobEncode() ([]byte, error) {
return (time.Time)(t).GobEncode()
}
func (t *UnixTime) GobDecode(data []byte) error {
return (*time.Time)(t).GobDecode(data)
}
func (t *UnixTime) UnmarshalJSON(data []byte) error {
str := ""
if err := json.Unmarshal(data, &str); err != nil {
return err
}
t0, err := strconv.ParseInt(str, 10, 64)
if err != nil {
return err
}
*t = UnixTime(time.Unix(t0, 0))
return nil
}
func (t UnixTime) MarshalJSON() ([]byte, error) {
str := strconv.FormatInt(t.Time().Unix(), 10)
return json.Marshal(str)
}
func (t UnixTime) MarshalText() ([]byte, error) {
return []byte(strconv.FormatInt(t.Time().Unix(), 10)), nil
}
func (t *UnixTime) UnmarshalText(data []byte) error {
t0, err := strconv.ParseInt(string(data), 10, 64)
if err != nil {
return err
}
*t = UnixTime(time.Unix(t0, 0))
return nil
}
func (t UnixTime) Serialize() string {
return strconv.FormatInt(t.Time().Unix(), 10)
}
func (t UnixTime) After(u RFCTime) bool {
return t.Time().After(u.Time())
}
func (t UnixTime) Before(u RFCTime) bool {
return t.Time().Before(u.Time())
}
func (t UnixTime) Equal(u RFCTime) bool {
return t.Time().Equal(u.Time())
}
func (t UnixTime) IsZero() bool {
return t.Time().IsZero()
}
func (t UnixTime) Date() (year int, month time.Month, day int) {
return t.Time().Date()
}
func (t UnixTime) Year() int {
return t.Time().Year()
}
func (t UnixTime) Month() time.Month {
return t.Time().Month()
}
func (t UnixTime) Day() int {
return t.Time().Day()
}
func (t UnixTime) Weekday() time.Weekday {
return t.Time().Weekday()
}
func (t UnixTime) ISOWeek() (year, week int) {
return t.Time().ISOWeek()
}
func (t UnixTime) Clock() (hour, min, sec int) {
return t.Time().Clock()
}
func (t UnixTime) Hour() int {
return t.Time().Hour()
}
func (t UnixTime) Minute() int {
return t.Time().Minute()
}
func (t UnixTime) Second() int {
return t.Time().Second()
}
func (t UnixTime) Nanosecond() int {
return t.Time().Nanosecond()
}
func (t UnixTime) YearDay() int {
return t.Time().YearDay()
}
func (t UnixTime) Add(d time.Duration) UnixTime {
return UnixTime(t.Time().Add(d))
}
func (t UnixTime) Sub(u RFCTime) time.Duration {
return t.Time().Sub(u.Time())
}
func (t UnixTime) AddDate(years int, months int, days int) UnixTime {
return UnixTime(t.Time().AddDate(years, months, days))
}
func (t UnixTime) Unix() int64 {
return t.Time().Unix()
}
func (t UnixTime) UnixMilli() int64 {
return t.Time().UnixMilli()
}
func (t UnixTime) UnixMicro() int64 {
return t.Time().UnixMicro()
}
func (t UnixTime) UnixNano() int64 {
return t.Time().UnixNano()
}
func (t UnixTime) Format(layout string) string {
return t.Time().Format(layout)
}
func (t UnixTime) GoString() string {
return t.Time().GoString()
}
func (t UnixTime) String() string {
return t.Time().String()
}
func NewUnix(t time.Time) UnixTime {
return UnixTime(t)
}
func NowUnix() UnixTime {
return UnixTime(time.Now())
}

176
rfctime/unixMilli.go Normal file
View File

@@ -0,0 +1,176 @@
package rfctime
import (
"encoding/json"
"strconv"
"time"
)
type UnixMilliTime time.Time
func (t UnixMilliTime) Time() time.Time {
return time.Time(t)
}
func (t UnixMilliTime) MarshalBinary() ([]byte, error) {
return (time.Time)(t).MarshalBinary()
}
func (t *UnixMilliTime) UnmarshalBinary(data []byte) error {
return (*time.Time)(t).UnmarshalBinary(data)
}
func (t UnixMilliTime) GobEncode() ([]byte, error) {
return (time.Time)(t).GobEncode()
}
func (t *UnixMilliTime) GobDecode(data []byte) error {
return (*time.Time)(t).GobDecode(data)
}
func (t *UnixMilliTime) UnmarshalJSON(data []byte) error {
str := ""
if err := json.Unmarshal(data, &str); err != nil {
return err
}
t0, err := strconv.ParseInt(str, 10, 64)
if err != nil {
return err
}
*t = UnixMilliTime(time.UnixMilli(t0))
return nil
}
func (t UnixMilliTime) MarshalJSON() ([]byte, error) {
str := strconv.FormatInt(t.Time().UnixMilli(), 10)
return json.Marshal(str)
}
func (t UnixMilliTime) MarshalText() ([]byte, error) {
return []byte(strconv.FormatInt(t.Time().UnixMilli(), 10)), nil
}
func (t *UnixMilliTime) UnmarshalText(data []byte) error {
t0, err := strconv.ParseInt(string(data), 10, 64)
if err != nil {
return err
}
*t = UnixMilliTime(time.UnixMilli(t0))
return nil
}
func (t UnixMilliTime) Serialize() string {
return strconv.FormatInt(t.Time().UnixMilli(), 10)
}
func (t UnixMilliTime) After(u RFCTime) bool {
return t.Time().After(u.Time())
}
func (t UnixMilliTime) Before(u RFCTime) bool {
return t.Time().Before(u.Time())
}
func (t UnixMilliTime) Equal(u RFCTime) bool {
return t.Time().Equal(u.Time())
}
func (t UnixMilliTime) IsZero() bool {
return t.Time().IsZero()
}
func (t UnixMilliTime) Date() (year int, month time.Month, day int) {
return t.Time().Date()
}
func (t UnixMilliTime) Year() int {
return t.Time().Year()
}
func (t UnixMilliTime) Month() time.Month {
return t.Time().Month()
}
func (t UnixMilliTime) Day() int {
return t.Time().Day()
}
func (t UnixMilliTime) Weekday() time.Weekday {
return t.Time().Weekday()
}
func (t UnixMilliTime) ISOWeek() (year, week int) {
return t.Time().ISOWeek()
}
func (t UnixMilliTime) Clock() (hour, min, sec int) {
return t.Time().Clock()
}
func (t UnixMilliTime) Hour() int {
return t.Time().Hour()
}
func (t UnixMilliTime) Minute() int {
return t.Time().Minute()
}
func (t UnixMilliTime) Second() int {
return t.Time().Second()
}
func (t UnixMilliTime) Nanosecond() int {
return t.Time().Nanosecond()
}
func (t UnixMilliTime) YearDay() int {
return t.Time().YearDay()
}
func (t UnixMilliTime) Add(d time.Duration) UnixMilliTime {
return UnixMilliTime(t.Time().Add(d))
}
func (t UnixMilliTime) Sub(u RFCTime) time.Duration {
return t.Time().Sub(u.Time())
}
func (t UnixMilliTime) AddDate(years int, months int, days int) UnixMilliTime {
return UnixMilliTime(t.Time().AddDate(years, months, days))
}
func (t UnixMilliTime) Unix() int64 {
return t.Time().Unix()
}
func (t UnixMilliTime) UnixMilli() int64 {
return t.Time().UnixMilli()
}
func (t UnixMilliTime) UnixMicro() int64 {
return t.Time().UnixMicro()
}
func (t UnixMilliTime) UnixNano() int64 {
return t.Time().UnixNano()
}
func (t UnixMilliTime) Format(layout string) string {
return t.Time().Format(layout)
}
func (t UnixMilliTime) GoString() string {
return t.Time().GoString()
}
func (t UnixMilliTime) String() string {
return t.Time().String()
}
func NewUnixMilli(t time.Time) UnixMilliTime {
return UnixMilliTime(t)
}
func NowUnixMilli() UnixMilliTime {
return UnixMilliTime(time.Now())
}

176
rfctime/unixNano.go Normal file
View File

@@ -0,0 +1,176 @@
package rfctime
import (
"encoding/json"
"strconv"
"time"
)
type UnixNanoTime time.Time
func (t UnixNanoTime) Time() time.Time {
return time.Time(t)
}
func (t UnixNanoTime) MarshalBinary() ([]byte, error) {
return (time.Time)(t).MarshalBinary()
}
func (t *UnixNanoTime) UnmarshalBinary(data []byte) error {
return (*time.Time)(t).UnmarshalBinary(data)
}
func (t UnixNanoTime) GobEncode() ([]byte, error) {
return (time.Time)(t).GobEncode()
}
func (t *UnixNanoTime) GobDecode(data []byte) error {
return (*time.Time)(t).GobDecode(data)
}
func (t *UnixNanoTime) UnmarshalJSON(data []byte) error {
str := ""
if err := json.Unmarshal(data, &str); err != nil {
return err
}
t0, err := strconv.ParseInt(str, 10, 64)
if err != nil {
return err
}
*t = UnixNanoTime(time.Unix(0, t0))
return nil
}
func (t UnixNanoTime) MarshalJSON() ([]byte, error) {
str := strconv.FormatInt(t.Time().UnixNano(), 10)
return json.Marshal(str)
}
func (t UnixNanoTime) MarshalText() ([]byte, error) {
return []byte(strconv.FormatInt(t.Time().UnixNano(), 10)), nil
}
func (t *UnixNanoTime) UnmarshalText(data []byte) error {
t0, err := strconv.ParseInt(string(data), 10, 64)
if err != nil {
return err
}
*t = UnixNanoTime(time.Unix(0, t0))
return nil
}
func (t UnixNanoTime) Serialize() string {
return strconv.FormatInt(t.Time().UnixNano(), 10)
}
func (t UnixNanoTime) After(u RFCTime) bool {
return t.Time().After(u.Time())
}
func (t UnixNanoTime) Before(u RFCTime) bool {
return t.Time().Before(u.Time())
}
func (t UnixNanoTime) Equal(u RFCTime) bool {
return t.Time().Equal(u.Time())
}
func (t UnixNanoTime) IsZero() bool {
return t.Time().IsZero()
}
func (t UnixNanoTime) Date() (year int, month time.Month, day int) {
return t.Time().Date()
}
func (t UnixNanoTime) Year() int {
return t.Time().Year()
}
func (t UnixNanoTime) Month() time.Month {
return t.Time().Month()
}
func (t UnixNanoTime) Day() int {
return t.Time().Day()
}
func (t UnixNanoTime) Weekday() time.Weekday {
return t.Time().Weekday()
}
func (t UnixNanoTime) ISOWeek() (year, week int) {
return t.Time().ISOWeek()
}
func (t UnixNanoTime) Clock() (hour, min, sec int) {
return t.Time().Clock()
}
func (t UnixNanoTime) Hour() int {
return t.Time().Hour()
}
func (t UnixNanoTime) Minute() int {
return t.Time().Minute()
}
func (t UnixNanoTime) Second() int {
return t.Time().Second()
}
func (t UnixNanoTime) Nanosecond() int {
return t.Time().Nanosecond()
}
func (t UnixNanoTime) YearDay() int {
return t.Time().YearDay()
}
func (t UnixNanoTime) Add(d time.Duration) UnixNanoTime {
return UnixNanoTime(t.Time().Add(d))
}
func (t UnixNanoTime) Sub(u RFCTime) time.Duration {
return t.Time().Sub(u.Time())
}
func (t UnixNanoTime) AddDate(years int, months int, days int) UnixNanoTime {
return UnixNanoTime(t.Time().AddDate(years, months, days))
}
func (t UnixNanoTime) Unix() int64 {
return t.Time().Unix()
}
func (t UnixNanoTime) UnixMilli() int64 {
return t.Time().UnixNano()
}
func (t UnixNanoTime) UnixMicro() int64 {
return t.Time().UnixMicro()
}
func (t UnixNanoTime) UnixNano() int64 {
return t.Time().UnixNano()
}
func (t UnixNanoTime) Format(layout string) string {
return t.Time().Format(layout)
}
func (t UnixNanoTime) GoString() string {
return t.Time().GoString()
}
func (t UnixNanoTime) String() string {
return t.Time().String()
}
func NewUnixNano(t time.Time) UnixNanoTime {
return UnixNanoTime(t)
}
func NowUnixNano() UnixNanoTime {
return UnixNanoTime(time.Now())
}

128
sq/database.go Normal file
View File

@@ -0,0 +1,128 @@
package sq
import (
"context"
"database/sql"
"github.com/jmoiron/sqlx"
"sync"
)
type DB interface {
Exec(ctx context.Context, sql string, prep PP) (sql.Result, error)
Query(ctx context.Context, sql string, prep PP) (*sqlx.Rows, error)
Ping(ctx context.Context) error
BeginTransaction(ctx context.Context, iso sql.IsolationLevel) (Tx, error)
AddListener(listener Listener)
Exit() error
}
type database struct {
db *sqlx.DB
txctr uint16
lock sync.Mutex
lstr []Listener
}
func NewDB(db *sqlx.DB) DB {
return &database{
db: db,
txctr: 0,
lock: sync.Mutex{},
lstr: make([]Listener, 0),
}
}
func (db *database) AddListener(listener Listener) {
db.lstr = append(db.lstr, listener)
}
func (db *database) Exec(ctx context.Context, sqlstr string, prep PP) (sql.Result, error) {
origsql := sqlstr
for _, v := range db.lstr {
err := v.PreExec(ctx, nil, &sqlstr, &prep)
if err != nil {
return nil, err
}
}
res, err := db.db.NamedExecContext(ctx, sqlstr, prep)
for _, v := range db.lstr {
v.PostExec(nil, origsql, sqlstr, prep)
}
if err != nil {
return nil, err
}
return res, nil
}
func (db *database) Query(ctx context.Context, sqlstr string, prep PP) (*sqlx.Rows, error) {
origsql := sqlstr
for _, v := range db.lstr {
err := v.PreQuery(ctx, nil, &sqlstr, &prep)
if err != nil {
return nil, err
}
}
rows, err := sqlx.NamedQueryContext(ctx, db.db, sqlstr, prep)
for _, v := range db.lstr {
v.PostQuery(nil, origsql, sqlstr, prep)
}
if err != nil {
return nil, err
}
return rows, nil
}
func (db *database) Ping(ctx context.Context) error {
for _, v := range db.lstr {
err := v.PrePing(ctx)
if err != nil {
return err
}
}
err := db.db.PingContext(ctx)
for _, v := range db.lstr {
v.PostPing(err)
}
if err != nil {
return err
}
return nil
}
func (db *database) BeginTransaction(ctx context.Context, iso sql.IsolationLevel) (Tx, error) {
db.lock.Lock()
txid := db.txctr
db.txctr += 1 // with overflow !
db.lock.Unlock()
for _, v := range db.lstr {
err := v.PreTxBegin(ctx, txid)
if err != nil {
return nil, err
}
}
xtx, err := db.db.BeginTxx(ctx, &sql.TxOptions{Isolation: iso})
if err != nil {
return nil, err
}
for _, v := range db.lstr {
v.PostTxBegin(txid, err)
}
return NewTransaction(xtx, txid, db.lstr), nil
}
func (db *database) Exit() error {
return db.db.Close()
}

19
sq/listener.go Normal file
View File

@@ -0,0 +1,19 @@
package sq
import "context"
type Listener interface {
PrePing(ctx context.Context) error
PreTxBegin(ctx context.Context, txid uint16) error
PreTxCommit(txid uint16) error
PreTxRollback(txid uint16) error
PreQuery(ctx context.Context, txID *uint16, sql *string, params *PP) error
PreExec(ctx context.Context, txID *uint16, sql *string, params *PP) error
PostPing(result error)
PostTxBegin(txid uint16, result error)
PostTxCommit(txid uint16, result error)
PostTxRollback(txid uint16, result error)
PostQuery(txID *uint16, sqlOriginal string, sqlReal string, params PP)
PostExec(txID *uint16, sqlOriginal string, sqlReal string, params PP)
}

13
sq/params.go Normal file
View File

@@ -0,0 +1,13 @@
package sq
type PP map[string]any
func Join(pps ...PP) PP {
r := PP{}
for _, add := range pps {
for k, v := range add {
r[k] = v
}
}
return r
}

12
sq/queryable.go Normal file
View File

@@ -0,0 +1,12 @@
package sq
import (
"context"
"database/sql"
"github.com/jmoiron/sqlx"
)
type Queryable interface {
Exec(ctx context.Context, sql string, prep PP) (sql.Result, error)
Query(ctx context.Context, sql string, prep PP) (*sqlx.Rows, error)
}

140
sq/scanner.go Normal file
View File

@@ -0,0 +1,140 @@
package sq
import (
"database/sql"
"errors"
"github.com/jmoiron/sqlx"
)
type StructScanMode string
const (
SModeFast StructScanMode = "FAST"
SModeExtended StructScanMode = "EXTENDED"
)
type StructScanSafety string
const (
Safe StructScanSafety = "SAFE"
Unsafe StructScanSafety = "UNSAFE"
)
func ScanSingle[TData any](rows *sqlx.Rows, mode StructScanMode, sec StructScanSafety, close bool) (TData, error) {
if rows.Next() {
var strscan *StructScanner
if sec == Safe {
strscan = NewStructScanner(rows, false)
var data TData
err := strscan.Start(&data)
if err != nil {
return *new(TData), err
}
} else if sec == Unsafe {
strscan = NewStructScanner(rows, true)
var data TData
err := strscan.Start(&data)
if err != nil {
return *new(TData), err
}
} else {
return *new(TData), errors.New("unknown value for <sec>")
}
var data TData
if mode == SModeFast {
err := strscan.StructScanBase(&data)
if err != nil {
return *new(TData), err
}
} else if mode == SModeExtended {
err := strscan.StructScanExt(&data)
if err != nil {
return *new(TData), err
}
} else {
return *new(TData), errors.New("unknown value for <mode>")
}
if rows.Next() {
if close {
_ = rows.Close()
}
return *new(TData), errors.New("sql returned more than one row")
}
if close {
err := rows.Close()
if err != nil {
return *new(TData), err
}
}
if err := rows.Err(); err != nil {
return *new(TData), err
}
return data, nil
} else {
if close {
_ = rows.Close()
}
return *new(TData), sql.ErrNoRows
}
}
func ScanAll[TData any](rows *sqlx.Rows, mode StructScanMode, sec StructScanSafety, close bool) ([]TData, error) {
var strscan *StructScanner
if sec == Safe {
strscan = NewStructScanner(rows, false)
var data TData
err := strscan.Start(&data)
if err != nil {
return nil, err
}
} else if sec == Unsafe {
strscan = NewStructScanner(rows, true)
var data TData
err := strscan.Start(&data)
if err != nil {
return nil, err
}
} else {
return nil, errors.New("unknown value for <sec>")
}
res := make([]TData, 0)
for rows.Next() {
if mode == SModeFast {
var data TData
err := strscan.StructScanBase(&data)
if err != nil {
return nil, err
}
res = append(res, data)
} else if mode == SModeExtended {
var data TData
err := strscan.StructScanExt(&data)
if err != nil {
return nil, err
}
res = append(res, data)
} else {
return nil, errors.New("unknown value for <mode>")
}
}
if close {
err := strscan.rows.Close()
if err != nil {
return nil, err
}
}
if err := rows.Err(); err != nil {
return nil, err
}
return res, nil
}

223
sq/structscanner.go Normal file
View File

@@ -0,0 +1,223 @@
package sq
import (
"errors"
"fmt"
"github.com/jmoiron/sqlx"
"github.com/jmoiron/sqlx/reflectx"
"reflect"
)
// forked from sqlx, but added ability to unmarshal optional-nested structs
type StructScanner struct {
rows *sqlx.Rows
Mapper *reflectx.Mapper
unsafe bool
fields [][]int
values []any
columns []string
}
func NewStructScanner(rows *sqlx.Rows, unsafe bool) *StructScanner {
return &StructScanner{
rows: rows,
Mapper: reflectx.NewMapper("db"),
unsafe: unsafe,
}
}
func (r *StructScanner) Start(dest any) error {
v := reflect.ValueOf(dest)
if v.Kind() != reflect.Ptr {
return errors.New("must pass a pointer, not a value, to StructScan destination")
}
columns, err := r.rows.Columns()
if err != nil {
return err
}
r.columns = columns
r.fields = r.Mapper.TraversalsByName(v.Type(), columns)
// if we are not unsafe and are missing fields, return an error
if f, err := missingFields(r.fields); err != nil && !r.unsafe {
return fmt.Errorf("missing destination name %s in %T", columns[f], dest)
}
r.values = make([]interface{}, len(columns))
return nil
}
// StructScanExt forked from github.com/jmoiron/sqlx@v1.3.5/sqlx.go
// does also wok with nullabel structs (from LEFT JOIN's)
func (r *StructScanner) StructScanExt(dest any) error {
v := reflect.ValueOf(dest)
if v.Kind() != reflect.Ptr {
return errors.New("must pass a pointer, not a value, to StructScan destination")
}
// ========= STEP 1 :: =========
v = v.Elem()
err := fieldsByTraversalExtended(v, r.fields, r.values)
if err != nil {
return err
}
// scan into the struct field pointers and append to our results
err = r.rows.Scan(r.values...)
if err != nil {
return err
}
nullStructs := make(map[string]bool)
for i, traversal := range r.fields {
if len(traversal) == 0 {
continue
}
isnsil := reflect.ValueOf(r.values[i]).Elem().IsNil()
for i := 1; i < len(traversal); i++ {
canParentNil := reflectx.FieldByIndexes(v, traversal[0:i]).Kind() == reflect.Pointer
k := fmt.Sprintf("%v", traversal[0:i])
if v, ok := nullStructs[k]; ok {
nullStructs[k] = canParentNil && v && isnsil
} else {
nullStructs[k] = canParentNil && isnsil
}
}
}
forcenulled := make(map[string]bool)
for i, traversal := range r.fields {
if len(traversal) == 0 {
continue
}
anyparentnull := false
for i := 1; i < len(traversal); i++ {
k := fmt.Sprintf("%v", traversal[0:i])
if nv, ok := nullStructs[k]; ok && nv {
if _, ok := forcenulled[k]; !ok {
f := reflectx.FieldByIndexes(v, traversal[0:i])
f.Set(reflect.Zero(f.Type())) // set to nil
forcenulled[k] = true
}
anyparentnull = true
break
}
}
if anyparentnull {
continue
}
f := reflectx.FieldByIndexes(v, traversal)
val1 := reflect.ValueOf(r.values[i])
val2 := val1.Elem()
val3 := val2.Elem()
if val2.IsNil() {
if f.Kind() != reflect.Pointer {
return errors.New(fmt.Sprintf("Cannot set field %v to NULL value from column '%s' (type: %s)", traversal, r.columns[i], f.Type().String()))
}
f.Set(reflect.Zero(f.Type())) // set to nil
} else {
f.Set(val3)
}
}
return r.rows.Err()
}
// StructScanBase forked from github.com/jmoiron/sqlx@v1.3.5/sqlx.go
// without (relevant) changes
func (r *StructScanner) StructScanBase(dest any) error {
v := reflect.ValueOf(dest)
if v.Kind() != reflect.Ptr {
return errors.New("must pass a pointer, not a value, to StructScan destination")
}
v = v.Elem()
err := fieldsByTraversalBase(v, r.fields, r.values, true)
if err != nil {
return err
}
// scan into the struct field pointers and append to our results
err = r.rows.Scan(r.values...)
if err != nil {
return err
}
return r.rows.Err()
}
// fieldsByTraversal forked from github.com/jmoiron/sqlx@v1.3.5/sqlx.go
func fieldsByTraversalExtended(v reflect.Value, traversals [][]int, values []interface{}) error {
v = reflect.Indirect(v)
if v.Kind() != reflect.Struct {
return errors.New("argument not a struct")
}
for i, traversal := range traversals {
if len(traversal) == 0 {
values[i] = new(interface{})
continue
}
f := reflectx.FieldByIndexes(v, traversal)
values[i] = reflect.New(reflect.PointerTo(f.Type())).Interface()
}
return nil
}
// fieldsByTraversal forked from github.com/jmoiron/sqlx@v1.3.5/sqlx.go
func fieldsByTraversalBase(v reflect.Value, traversals [][]int, values []interface{}, ptrs bool) error {
v = reflect.Indirect(v)
if v.Kind() != reflect.Struct {
return errors.New("argument not a struct")
}
for i, traversal := range traversals {
if len(traversal) == 0 {
values[i] = new(interface{})
continue
}
f := reflectx.FieldByIndexes(v, traversal)
if ptrs {
values[i] = f.Addr().Interface()
} else {
values[i] = f.Interface()
}
}
return nil
}
// missingFields forked from github.com/jmoiron/sqlx@v1.3.5/sqlx.go
func missingFields(transversals [][]int) (field int, err error) {
for i, t := range transversals {
if len(t) == 0 {
return i, errors.New("missing field")
}
}
return 0, nil
}

105
sq/transaction.go Normal file
View File

@@ -0,0 +1,105 @@
package sq
import (
"context"
"database/sql"
"github.com/jmoiron/sqlx"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
)
type Tx interface {
Rollback() error
Commit() error
Exec(ctx context.Context, sql string, prep PP) (sql.Result, error)
Query(ctx context.Context, sql string, prep PP) (*sqlx.Rows, error)
}
type transaction struct {
tx *sqlx.Tx
id uint16
lstr []Listener
}
func NewTransaction(xtx *sqlx.Tx, txid uint16, lstr []Listener) Tx {
return &transaction{
tx: xtx,
id: txid,
lstr: lstr,
}
}
func (tx *transaction) Rollback() error {
for _, v := range tx.lstr {
err := v.PreTxRollback(tx.id)
if err != nil {
return err
}
}
result := tx.tx.Rollback()
for _, v := range tx.lstr {
v.PostTxRollback(tx.id, result)
}
return result
}
func (tx *transaction) Commit() error {
for _, v := range tx.lstr {
err := v.PreTxCommit(tx.id)
if err != nil {
return err
}
}
result := tx.tx.Commit()
for _, v := range tx.lstr {
v.PostTxRollback(tx.id, result)
}
return result
}
func (tx *transaction) Exec(ctx context.Context, sqlstr string, prep PP) (sql.Result, error) {
origsql := sqlstr
for _, v := range tx.lstr {
err := v.PreExec(ctx, langext.Ptr(tx.id), &sqlstr, &prep)
if err != nil {
return nil, err
}
}
res, err := tx.tx.NamedExecContext(ctx, sqlstr, prep)
for _, v := range tx.lstr {
v.PostExec(langext.Ptr(tx.id), origsql, sqlstr, prep)
}
if err != nil {
return nil, err
}
return res, nil
}
func (tx *transaction) Query(ctx context.Context, sqlstr string, prep PP) (*sqlx.Rows, error) {
origsql := sqlstr
for _, v := range tx.lstr {
err := v.PreQuery(ctx, langext.Ptr(tx.id), &sqlstr, &prep)
if err != nil {
return nil, err
}
}
rows, err := sqlx.NamedQueryContext(ctx, tx.tx, sqlstr, prep)
for _, v := range tx.lstr {
v.PostQuery(langext.Ptr(tx.id), origsql, sqlstr, prep)
}
if err != nil {
return nil, err
}
return rows, nil
}

109
syncext/atomic.go Normal file
View File

@@ -0,0 +1,109 @@
package syncext
import (
"context"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"sync"
"time"
)
type AtomicBool struct {
v bool
listener map[string]chan bool
lock sync.Mutex
}
func NewAtomicBool(value bool) *AtomicBool {
return &AtomicBool{
v: value,
listener: make(map[string]chan bool),
lock: sync.Mutex{},
}
}
func (a *AtomicBool) Get() bool {
a.lock.Lock()
defer a.lock.Unlock()
return a.v
}
func (a *AtomicBool) Set(value bool) {
a.lock.Lock()
defer a.lock.Unlock()
a.v = value
for k, v := range a.listener {
select {
case v <- value:
// message sent
default:
// no receiver on channel
delete(a.listener, k)
}
}
}
func (a *AtomicBool) Wait(waitFor bool) {
_ = a.WaitWithContext(context.Background(), waitFor)
}
func (a *AtomicBool) WaitWithTimeout(timeout time.Duration, waitFor bool) error {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
return a.WaitWithContext(ctx, waitFor)
}
func (a *AtomicBool) WaitWithContext(ctx context.Context, waitFor bool) error {
if err := ctx.Err(); err != nil {
return err
}
if a.Get() == waitFor {
return nil
}
uuid, _ := langext.NewHexUUID()
waitchan := make(chan bool)
a.lock.Lock()
a.listener[uuid] = waitchan
a.lock.Unlock()
defer func() {
a.lock.Lock()
delete(a.listener, uuid)
a.lock.Unlock()
}()
for {
if err := ctx.Err(); err != nil {
return err
}
timeOut := 1024 * time.Millisecond
if dl, ok := ctx.Deadline(); ok {
timeOutMax := dl.Sub(time.Now())
if timeOutMax <= 0 {
timeOut = 0
} else if 0 < timeOutMax && timeOutMax < timeOut {
timeOut = timeOutMax
}
}
if v, ok := ReadChannelWithTimeout(waitchan, timeOut); ok {
if v == waitFor {
return nil
}
} else {
if err := ctx.Err(); err != nil {
return err
}
if a.Get() == waitFor {
return nil
}
}
}
}

45
syncext/channel.go Normal file
View File

@@ -0,0 +1,45 @@
package syncext
import (
"time"
)
// https://gobyexample.com/non-blocking-channel-operations
// https://gobyexample.com/timeouts
// https://groups.google.com/g/golang-nuts/c/Oth9CmJPoqo
func ReadChannelWithTimeout[T any](c chan T, timeout time.Duration) (T, bool) {
select {
case msg := <-c:
return msg, true
case <-time.After(timeout):
return *new(T), false
}
}
func WriteChannelWithTimeout[T any](c chan T, msg T, timeout time.Duration) bool {
select {
case c <- msg:
return true
case <-time.After(timeout):
return false
}
}
func ReadNonBlocking[T any](c chan T) (T, bool) {
select {
case msg := <-c:
return msg, true
default:
return *new(T), false
}
}
func WriteNonBlocking[T any](c chan T, msg T) bool {
select {
case c <- msg:
return true
default:
return false
}
}

121
syncext/channel_test.go Normal file
View File

@@ -0,0 +1,121 @@
package syncext
import (
"testing"
"time"
)
func TestTimeoutReadBuffered(t *testing.T) {
c := make(chan int, 1)
go func() {
time.Sleep(200 * time.Millisecond)
c <- 112
}()
_, ok := ReadChannelWithTimeout(c, 100*time.Millisecond)
if ok {
t.Error("Read success, but should timeout")
}
}
func TestTimeoutReadBigBuffered(t *testing.T) {
c := make(chan int, 128)
go func() {
time.Sleep(200 * time.Millisecond)
c <- 112
}()
_, ok := ReadChannelWithTimeout(c, 100*time.Millisecond)
if ok {
t.Error("Read success, but should timeout")
}
}
func TestTimeoutReadUnbuffered(t *testing.T) {
c := make(chan int)
go func() {
time.Sleep(200 * time.Millisecond)
c <- 112
}()
_, ok := ReadChannelWithTimeout(c, 100*time.Millisecond)
if ok {
t.Error("Read success, but should timeout")
}
}
func TestNoTimeoutAfterStartReadBuffered(t *testing.T) {
c := make(chan int, 1)
go func() {
time.Sleep(10 * time.Millisecond)
c <- 112
}()
_, ok := ReadChannelWithTimeout(c, 100*time.Millisecond)
if !ok {
t.Error("Read timeout, but should have succeeded")
}
}
func TestNoTimeoutAfterStartReadBigBuffered(t *testing.T) {
c := make(chan int, 128)
go func() {
time.Sleep(10 * time.Millisecond)
c <- 112
}()
_, ok := ReadChannelWithTimeout(c, 100*time.Millisecond)
if !ok {
t.Error("Read timeout, but should have succeeded")
}
}
func TestNoTimeoutAfterStartReadUnbuffered(t *testing.T) {
c := make(chan int)
go func() {
time.Sleep(10 * time.Millisecond)
c <- 112
}()
_, ok := ReadChannelWithTimeout(c, 100*time.Millisecond)
if !ok {
t.Error("Read timeout, but should have succeeded")
}
}
func TestNoTimeoutBeforeStartReadBuffered(t *testing.T) {
c := make(chan int, 1)
c <- 112
_, ok := ReadChannelWithTimeout(c, 10*time.Millisecond)
if !ok {
t.Error("Read timeout, but should have succeeded")
}
}
func TestNoTimeoutBeforeStartReadBigBuffered(t *testing.T) {
c := make(chan int, 128)
c <- 112
_, ok := ReadChannelWithTimeout(c, 10*time.Millisecond)
if !ok {
t.Error("Read timeout, but should have succeeded")
}
}

61
termext/colors.go Normal file
View File

@@ -0,0 +1,61 @@
package termext
import "strings"
const (
colorReset = "\033[0m"
colorRed = "\033[31m"
colorGreen = "\033[32m"
colorYellow = "\033[33m"
colorBlue = "\033[34m"
colorPurple = "\033[35m"
colorCyan = "\033[36m"
colorGray = "\033[37m"
colorWhite = "\033[97m"
)
func Red(v string) string {
return colorRed + v + colorReset
}
func Green(v string) string {
return colorGreen + v + colorReset
}
func Yellow(v string) string {
return colorYellow + v + colorReset
}
func Blue(v string) string {
return colorBlue + v + colorReset
}
func Purple(v string) string {
return colorPurple + v + colorReset
}
func Cyan(v string) string {
return colorCyan + v + colorReset
}
func Gray(v string) string {
return colorGray + v + colorReset
}
func White(v string) string {
return colorWhite + v + colorReset
}
func CleanString(v string) string {
v = strings.ReplaceAll(v, colorReset, "")
v = strings.ReplaceAll(v, colorRed, "")
v = strings.ReplaceAll(v, colorGreen, "")
v = strings.ReplaceAll(v, colorYellow, "")
v = strings.ReplaceAll(v, colorBlue, "")
v = strings.ReplaceAll(v, colorPurple, "")
v = strings.ReplaceAll(v, colorCyan, "")
v = strings.ReplaceAll(v, colorGray, "")
v = strings.ReplaceAll(v, colorWhite, "")
return v
}

5
termext/osutil_darwin.go Normal file
View File

@@ -0,0 +1,5 @@
package termext
func enableColor() bool {
return true
}

View File

@@ -0,0 +1,5 @@
package termext
func enableColor() bool {
return true
}

5
termext/osutil_linux.go Normal file
View File

@@ -0,0 +1,5 @@
package termext
func enableColor() bool {
return true
}

5
termext/osutil_netbsd.go Normal file
View File

@@ -0,0 +1,5 @@
package termext
func enableColor() bool {
return true
}

View File

@@ -0,0 +1,5 @@
package termext
func enableColor() bool {
return true
}

26
termext/osutil_windows.go Normal file
View File

@@ -0,0 +1,26 @@
package termext
import "golang.org/x/sys/windows"
func enableColor() bool {
handle, err := windows.GetStdHandle(windows.STD_OUTPUT_HANDLE)
if err != nil {
return false
}
var mode uint32
err = windows.GetConsoleMode(handle, &mode)
if err != nil {
return false
}
if mode&windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING != windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING {
mode = mode | windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING
err = windows.SetConsoleMode(handle, mode)
if err != nil {
return false
}
}
return true
}

90
termext/termcolor.go Normal file
View File

@@ -0,0 +1,90 @@
package termext
import (
"golang.org/x/term"
"os"
"regexp"
"strconv"
"strings"
)
// -> partly copied from [ https://github.com/jwalton/go-supportscolor/tree/master ]
func SupportsColors() bool {
if isatty := term.IsTerminal(int(os.Stdout.Fd())); !isatty {
return false
}
termenv := os.Getenv("TERM")
if termenv == "dumb" {
return false
}
if osColorEnabled := enableColor(); !osColorEnabled {
return false
}
if _, ci := os.LookupEnv("CI"); ci {
var ciEnvNames = []string{"TRAVIS", "CIRCLECI", "APPVEYOR", "GITLAB_CI", "GITHUB_ACTIONS", "BUILDKITE", "DRONE"}
for _, ciEnvName := range ciEnvNames {
_, exists := os.LookupEnv(ciEnvName)
if exists {
return true
}
}
if os.Getenv("CI_NAME") == "codeship" {
return true
}
return false
}
if teamCityVersion, isTeamCity := os.LookupEnv("TEAMCITY_VERSION"); isTeamCity {
versionRegex := regexp.MustCompile(`^(9\.(0*[1-9]\d*)\.|\d{2,}\.)`)
if versionRegex.MatchString(teamCityVersion) {
return true
}
return false
}
if os.Getenv("COLORTERM") == "truecolor" {
return true
}
if termProgram, termProgramPreset := os.LookupEnv("TERM_PROGRAM"); termProgramPreset {
switch termProgram {
case "iTerm.app":
termProgramVersion := strings.Split(os.Getenv("TERM_PROGRAM_VERSION"), ".")
version, err := strconv.ParseInt(termProgramVersion[0], 10, 64)
if err == nil && version >= 3 {
return true
}
return true
case "Apple_Terminal":
return true
default:
// No default
}
}
var term256Regex = regexp.MustCompile("(?i)-256(color)?$")
if term256Regex.MatchString(termenv) {
return true
}
var termBasicRegex = regexp.MustCompile("(?i)^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux")
if termBasicRegex.MatchString(termenv) {
return true
}
if _, colorTerm := os.LookupEnv("COLORTERM"); colorTerm {
return true
}
return false
}

40
termext/termcolor_test.go Normal file
View File

@@ -0,0 +1,40 @@
package termext
import (
"math/rand"
"testing"
)
func init() {
rand.Seed(0)
}
func TestSupportsColors(t *testing.T) {
SupportsColors() // should not error
}
func TestColor(t *testing.T) {
assertEqual(t, Red("test"), "\033[31mtest\u001B[0m")
assertEqual(t, Green("test"), "\033[32mtest\u001B[0m")
assertEqual(t, Yellow("test"), "\033[33mtest\u001B[0m")
assertEqual(t, Blue("test"), "\033[34mtest\u001B[0m")
assertEqual(t, Purple("test"), "\033[35mtest\u001B[0m")
assertEqual(t, Cyan("test"), "\033[36mtest\u001B[0m")
assertEqual(t, Gray("test"), "\033[37mtest\u001B[0m")
assertEqual(t, White("test"), "\033[97mtest\u001B[0m")
assertEqual(t, CleanString(Red("test")), "test")
assertEqual(t, CleanString(Green("test")), "test")
assertEqual(t, CleanString(Yellow("test")), "test")
assertEqual(t, CleanString(Blue("test")), "test")
assertEqual(t, CleanString(Purple("test")), "test")
assertEqual(t, CleanString(Cyan("test")), "test")
assertEqual(t, CleanString(Gray("test")), "test")
assertEqual(t, CleanString(White("test")), "test")
}
func assertEqual(t *testing.T, actual string, expected string) {
if actual != expected {
t.Errorf("values differ: Actual: '%v', Expected: '%v'", actual, expected)
}
}

68
timeext/duration.go Normal file
View File

@@ -0,0 +1,68 @@
package timeext
import (
"fmt"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"time"
)
func FromNanoseconds[T langext.NumberConstraint](v T) time.Duration {
return time.Duration(int64(float64(v) * float64(time.Nanosecond)))
}
func FromMicroseconds[T langext.NumberConstraint](v T) time.Duration {
return time.Duration(int64(float64(v) * float64(time.Microsecond)))
}
func FromMilliseconds[T langext.NumberConstraint](v T) time.Duration {
return time.Duration(int64(float64(v) * float64(time.Millisecond)))
}
func FromSeconds[T langext.NumberConstraint](v T) time.Duration {
return time.Duration(int64(float64(v) * float64(time.Second)))
}
func FromMinutes[T langext.NumberConstraint](v T) time.Duration {
return time.Duration(int64(float64(v) * float64(time.Minute)))
}
func FromHours[T langext.NumberConstraint](v T) time.Duration {
return time.Duration(int64(float64(v) * float64(time.Hour)))
}
func FromDays[T langext.NumberConstraint](v T) time.Duration {
return time.Duration(int64(float64(v) * float64(24) * float64(time.Hour)))
}
func FormatNaturalDurationEnglish(iv time.Duration) string {
if sec := int64(iv.Seconds()); sec < 180 {
if sec == 1 {
return "1 second ago"
} else {
return fmt.Sprintf("%d seconds ago", sec)
}
}
if min := int64(iv.Minutes()); min < 180 {
return fmt.Sprintf("%d minutes ago", min)
}
if hours := int64(iv.Hours()); hours < 72 {
return fmt.Sprintf("%d hours ago", hours)
}
if days := int64(iv.Hours() / 24.0); days < 21 {
return fmt.Sprintf("%d days ago", days)
}
if weeks := int64(iv.Hours() / 24.0 / 7.0); weeks < 12 {
return fmt.Sprintf("%d weeks ago", weeks)
}
if months := int64(iv.Hours() / 24.0 / 7.0 / 30); months < 36 {
return fmt.Sprintf("%d months ago", months)
}
years := int64(iv.Hours() / 24.0 / 7.0 / 365)
return fmt.Sprintf("%d years ago", years)
}

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