Compare commits

...

11 Commits

Author SHA1 Message Date
5f51173276 v0.0.595 fix zerlog channel for exerr [ZeroLogErrTraces] output and WRN errors
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m33s
2025-08-20 13:03:21 +02:00
1586314e3e v0.0.594 Add exerr OutputRaw(http.ResponseWriter) method
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m35s
2025-07-16 17:13:07 +02:00
254fe1556a v0.0.593 made PubSub more generic (namespace can be any comparable type)
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m54s
2025-07-16 12:50:36 +02:00
52e74b59f5 v0.0.592
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 3m30s
2025-07-16 12:46:18 +02:00
64f2cd7219 v0.0.591 implement namespaced PubSub Broker in dataext
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Has been cancelled
2025-07-16 12:44:55 +02:00
a29aec8fb5 v0.0.590 more rfctime equal fixes for chris
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m34s
2025-07-15 14:14:22 +02:00
8ea9b3f79f v0.0.589 improve Equal method of rfctime structs - prevents panic in cmp library
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 1m48s
2025-07-15 13:46:36 +02:00
a4b2a0589f v0.0.588
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m37s
2025-07-11 11:50:29 +02:00
4ef5f6059b v0.0.587
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m37s
2025-07-06 22:24:44 +02:00
b23a444aa2 v0.0.586
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m33s
2025-07-04 13:56:53 +02:00
09932046f8 v0.0.585
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m36s
2025-07-04 11:46:00 +02:00
21 changed files with 1195 additions and 85 deletions

View File

@@ -46,7 +46,7 @@ var rexEnumPackage = rext.W(regexp.MustCompile(`^package\s+(?P<name>[A-Za-z0-9_]
var rexEnumDef = rext.W(regexp.MustCompile(`^\s*type\s+(?P<name>[A-Za-z0-9_]+)\s+(?P<type>[A-Za-z0-9_]+)\s*//\s*(@enum:type).*$`))
var rexEnumValueDef = rext.W(regexp.MustCompile(`^\s*(?P<name>[A-Za-z0-9_]+)\s+(?P<type>[A-Za-z0-9_]+)\s*=\s*(?P<value>("[@A-Za-z0-9_:\s\-.]*"|[0-9]+))\s*(//(?P<comm>.*))?.*$`))
var rexEnumValueDef = rext.W(regexp.MustCompile(`^\s*(?P<name>[A-Za-z0-9_]+)\s+(?P<type>[A-Za-z0-9_]+)\s*=\s*(?P<value>("[/@A-Za-z0-9_:\s\-.]*"|[0-9]+))\s*(//(?P<comm>.*))?.*$`))
var rexEnumChecksumConst = rext.W(regexp.MustCompile(`const ChecksumEnumGenerator = "(?P<cs>[A-Za-z0-9_]*)"`))

241
dataext/pubsub.go Normal file
View File

@@ -0,0 +1,241 @@
package dataext
import (
"context"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
"git.blackforestbytes.com/BlackForestBytes/goext/syncext"
"github.com/rs/xid"
"iter"
"sync"
"time"
)
// PubSub is a simple Pub/Sub Broker
// Clients can subscribe to a namespace and receive published messages on this namespace
// Messages are broadcast to all subscribers
type PubSub[TNamespace comparable, TData any] struct {
masterLock *sync.Mutex
subscriptions map[TNamespace][]*pubSubSubscription[TNamespace, TData]
}
type PubSubSubscription interface {
Unsubscribe()
}
type pubSubSubscription[TNamespace comparable, TData any] struct {
ID string
parent *PubSub[TNamespace, TData]
namespace TNamespace
subLock *sync.Mutex
Func func(TData)
Chan chan TData
UnsubChan chan bool
}
func (p *pubSubSubscription[TNamespace, TData]) Unsubscribe() {
p.parent.unsubscribe(p)
}
func NewPubSub[TNamespace comparable, TData any](capacity int) *PubSub[TNamespace, TData] {
return &PubSub[TNamespace, TData]{
masterLock: &sync.Mutex{},
subscriptions: make(map[TNamespace][]*pubSubSubscription[TNamespace, TData], capacity),
}
}
func (ps *PubSub[TNamespace, TData]) Namespaces() []TNamespace {
ps.masterLock.Lock()
defer ps.masterLock.Unlock()
return langext.MapKeyArr(ps.subscriptions)
}
func (ps *PubSub[TNamespace, TData]) SubscriberCount(ns TNamespace) int {
ps.masterLock.Lock()
defer ps.masterLock.Unlock()
return len(ps.subscriptions[ns])
}
// Publish sends `data` to all subscriber
// But unbuffered - if one is currently not listening, we skip (the actualReceiver < subscriber)
func (ps *PubSub[TNamespace, TData]) Publish(ns TNamespace, data TData) (subscriber int, actualReceiver int) {
ps.masterLock.Lock()
subs := langext.ArrCopy(ps.subscriptions[ns])
ps.masterLock.Unlock()
subscriber = len(subs)
actualReceiver = 0
for _, sub := range subs {
func() {
sub.subLock.Lock()
defer sub.subLock.Unlock()
if sub.Func != nil {
go func() { sub.Func(data) }()
actualReceiver++
} else if sub.Chan != nil {
msgSent := syncext.WriteNonBlocking(sub.Chan, data)
if msgSent {
actualReceiver++
}
}
}()
}
return subscriber, actualReceiver
}
// PublishWithContext sends `data` to all subscriber
// buffered - if one is currently not listening, we wait (but error out when the context runs out)
func (ps *PubSub[TNamespace, TData]) PublishWithContext(ctx context.Context, ns TNamespace, data TData) (subscriber int, actualReceiver int, err error) {
ps.masterLock.Lock()
subs := langext.ArrCopy(ps.subscriptions[ns])
ps.masterLock.Unlock()
subscriber = len(subs)
actualReceiver = 0
for _, sub := range subs {
err := func() error {
sub.subLock.Lock()
defer sub.subLock.Unlock()
if err := ctx.Err(); err != nil {
return err
}
if sub.Func != nil {
go func() { sub.Func(data) }()
actualReceiver++
} else if sub.Chan != nil {
err := syncext.WriteChannelWithContext(ctx, sub.Chan, data)
if err != nil {
return err
}
actualReceiver++
}
return nil
}()
if err != nil {
return subscriber, actualReceiver, err
}
}
return subscriber, actualReceiver, nil
}
// PublishWithTimeout sends `data` to all subscriber
// buffered - if one is currently not listening, we wait (but wait at most `timeout` - if the timeout is exceeded then actualReceiver < subscriber)
func (ps *PubSub[TNamespace, TData]) PublishWithTimeout(ns TNamespace, data TData, timeout time.Duration) (subscriber int, actualReceiver int) {
ps.masterLock.Lock()
subs := langext.ArrCopy(ps.subscriptions[ns])
ps.masterLock.Unlock()
subscriber = len(subs)
actualReceiver = 0
for _, sub := range subs {
func() {
sub.subLock.Lock()
defer sub.subLock.Unlock()
if sub.Func != nil {
go func() { sub.Func(data) }()
actualReceiver++
} else if sub.Chan != nil {
ok := syncext.WriteChannelWithTimeout(sub.Chan, data, timeout)
if ok {
actualReceiver++
}
}
}()
}
return subscriber, actualReceiver
}
func (ps *PubSub[TNamespace, TData]) SubscribeByCallback(ns TNamespace, fn func(TData)) PubSubSubscription {
ps.masterLock.Lock()
defer ps.masterLock.Unlock()
sub := &pubSubSubscription[TNamespace, TData]{ID: xid.New().String(), namespace: ns, parent: ps, subLock: &sync.Mutex{}, Func: fn, UnsubChan: nil}
ps.subscriptions[ns] = append(ps.subscriptions[ns], sub)
return sub
}
func (ps *PubSub[TNamespace, TData]) SubscribeByChan(ns TNamespace, chanBufferSize int) (chan TData, PubSubSubscription) {
ps.masterLock.Lock()
defer ps.masterLock.Unlock()
msgCh := make(chan TData, chanBufferSize)
sub := &pubSubSubscription[TNamespace, TData]{ID: xid.New().String(), namespace: ns, parent: ps, subLock: &sync.Mutex{}, Chan: msgCh, UnsubChan: nil}
ps.subscriptions[ns] = append(ps.subscriptions[ns], sub)
return msgCh, sub
}
func (ps *PubSub[TNamespace, TData]) SubscribeByIter(ns TNamespace, chanBufferSize int) (iter.Seq[TData], PubSubSubscription) {
ps.masterLock.Lock()
defer ps.masterLock.Unlock()
msgCh := make(chan TData, chanBufferSize)
unsubChan := make(chan bool, 8)
sub := &pubSubSubscription[TNamespace, TData]{ID: xid.New().String(), namespace: ns, parent: ps, subLock: &sync.Mutex{}, Chan: msgCh, UnsubChan: unsubChan}
ps.subscriptions[ns] = append(ps.subscriptions[ns], sub)
iterFun := func(yield func(TData) bool) {
for {
select {
case msg := <-msgCh:
if !yield(msg) {
sub.Unsubscribe()
return
}
case <-sub.UnsubChan:
sub.Unsubscribe()
return
}
}
}
return iterFun, sub
}
func (ps *PubSub[TNamespace, TData]) unsubscribe(p *pubSubSubscription[TNamespace, TData]) {
ps.masterLock.Lock()
defer ps.masterLock.Unlock()
p.subLock.Lock()
defer p.subLock.Unlock()
if p.Chan != nil {
close(p.Chan)
p.Chan = nil
}
if p.UnsubChan != nil {
syncext.WriteNonBlocking(p.UnsubChan, true)
close(p.UnsubChan)
p.UnsubChan = nil
}
ps.subscriptions[p.namespace] = langext.ArrFilter(ps.subscriptions[p.namespace], func(v *pubSubSubscription[TNamespace, TData]) bool {
return v.ID != p.ID
})
if len(ps.subscriptions[p.namespace]) == 0 {
delete(ps.subscriptions, p.namespace)
}
}

428
dataext/pubsub_test.go Normal file
View File

@@ -0,0 +1,428 @@
package dataext
import (
"context"
"sync"
"testing"
"time"
)
func TestNewPubSub(t *testing.T) {
ps := NewPubSub[string, string](10)
if ps == nil {
t.Fatal("NewPubSub returned nil")
}
if ps.masterLock == nil {
t.Fatal("masterLock is nil")
}
if ps.subscriptions == nil {
t.Fatal("subscriptions is nil")
}
}
func TestPubSub_Namespaces(t *testing.T) {
ps := NewPubSub[string, string](10)
// Initially no namespaces
namespaces := ps.Namespaces()
if len(namespaces) != 0 {
t.Fatalf("Expected 0 namespaces, got %d", len(namespaces))
}
// Add a subscription to create a namespace
_, sub1 := ps.SubscribeByChan("test-ns1", 1)
defer sub1.Unsubscribe()
// Add another subscription to a different namespace
_, sub2 := ps.SubscribeByChan("test-ns2", 1)
defer sub2.Unsubscribe()
// Check namespaces
namespaces = ps.Namespaces()
if len(namespaces) != 2 {
t.Fatalf("Expected 2 namespaces, got %d", len(namespaces))
}
// Check if namespaces contain the expected values
found1, found2 := false, false
for _, ns := range namespaces {
if ns == "test-ns1" {
found1 = true
}
if ns == "test-ns2" {
found2 = true
}
}
if !found1 || !found2 {
t.Fatalf("Expected to find both namespaces, found ns1: %v, ns2: %v", found1, found2)
}
}
func TestPubSub_SubscribeByCallback(t *testing.T) {
ps := NewPubSub[string, string](10)
var received string
var wg sync.WaitGroup
wg.Add(1)
callback := func(msg string) {
received = msg
wg.Done()
}
sub := ps.SubscribeByCallback("test-ns", callback)
defer sub.Unsubscribe()
// Publish a message
subs, receivers := ps.Publish("test-ns", "hello")
if subs != 1 {
t.Fatalf("Expected 1 subscriber, got %d", subs)
}
if receivers != 1 {
t.Fatalf("Expected 1 receiver, got %d", receivers)
}
// Wait for the callback to be executed
wg.Wait()
if received != "hello" {
t.Fatalf("Expected to receive 'hello', got '%s'", received)
}
}
func TestPubSub_SubscribeByChan(t *testing.T) {
ps := NewPubSub[string, string](10)
ch, sub := ps.SubscribeByChan("test-ns", 1)
defer sub.Unsubscribe()
// Publish a message
subs, receivers := ps.Publish("test-ns", "hello")
if subs != 1 {
t.Fatalf("Expected 1 subscriber, got %d", subs)
}
if receivers != 1 {
t.Fatalf("Expected 1 receiver, got %d", receivers)
}
// Read from the channel with a timeout to avoid blocking
select {
case msg := <-ch:
if msg != "hello" {
t.Fatalf("Expected to receive 'hello', got '%s'", msg)
}
case <-time.After(time.Second):
t.Fatal("Timed out waiting for message")
}
}
func TestPubSub_SubscribeByIter(t *testing.T) {
ps := NewPubSub[string, string](10)
iterSeq, sub := ps.SubscribeByIter("test-ns", 1)
defer sub.Unsubscribe()
// Channel to communicate when message is received
done := make(chan bool)
received := false
// Start a goroutine to use the iterator
go func() {
for msg := range iterSeq {
if msg == "hello" {
received = true
done <- true
return // Stop iteration
}
}
}()
// Give time for the iterator to start
time.Sleep(100 * time.Millisecond)
// Publish a message
ps.Publish("test-ns", "hello")
// Wait for the message to be received or timeout
select {
case <-done:
if !received {
t.Fatal("Message was received but not 'hello'")
}
case <-time.After(time.Second):
t.Fatal("Timed out waiting for message")
}
subCount := ps.SubscriberCount("test-ns")
if subCount != 0 {
t.Fatalf("Expected 0 receivers, got %d", subCount)
}
}
func TestPubSub_Publish(t *testing.T) {
ps := NewPubSub[string, string](10)
// Test publishing to a namespace with no subscribers
subs, receivers := ps.Publish("empty-ns", "hello")
if subs != 0 {
t.Fatalf("Expected 0 subscribers, got %d", subs)
}
if receivers != 0 {
t.Fatalf("Expected 0 receivers, got %d", receivers)
}
// Add a subscriber
ch, sub := ps.SubscribeByChan("test-ns", 1)
defer sub.Unsubscribe()
// Publish a message
subs, receivers = ps.Publish("test-ns", "hello")
if subs != 1 {
t.Fatalf("Expected 1 subscriber, got %d", subs)
}
if receivers != 1 {
t.Fatalf("Expected 1 receiver, got %d", receivers)
}
// Verify the message was received
select {
case msg := <-ch:
if msg != "hello" {
t.Fatalf("Expected to receive 'hello', got '%s'", msg)
}
case <-time.After(time.Second):
t.Fatal("Timed out waiting for message")
}
// Test non-blocking behavior with a full channel
// First fill the channel
ps.Publish("test-ns", "fill")
// Now publish again - this should not block but skip the receiver
subs, receivers = ps.Publish("test-ns", "overflow")
if subs != 1 {
t.Fatalf("Expected 1 subscriber, got %d", subs)
}
// The receiver count might be 0 if the channel is full
// Drain the channel
<-ch
}
func TestPubSub_PublishWithTimeout(t *testing.T) {
ps := NewPubSub[string, string](10)
// Add a subscriber with a channel
ch, sub := ps.SubscribeByChan("test-ns", 1)
defer sub.Unsubscribe()
// Publish with a timeout
subs, receivers := ps.PublishWithTimeout("test-ns", "hello", 100*time.Millisecond)
if subs != 1 {
t.Fatalf("Expected 1 subscriber, got %d", subs)
}
if receivers != 1 {
t.Fatalf("Expected 1 receiver, got %d", receivers)
}
// Verify the message was received
select {
case msg := <-ch:
if msg != "hello" {
t.Fatalf("Expected to receive 'hello', got '%s'", msg)
}
case <-time.After(time.Second):
t.Fatal("Timed out waiting for message")
}
// Fill the channel
ps.Publish("test-ns", "fill")
// Test timeout behavior with a full channel
start := time.Now()
subs, receivers = ps.PublishWithTimeout("test-ns", "timeout-test", 50*time.Millisecond)
elapsed := time.Since(start)
if subs != 1 {
t.Fatalf("Expected 1 subscriber, got %d", subs)
}
// The receiver count should be 0 if the timeout occurred
if elapsed < 50*time.Millisecond {
t.Fatalf("Expected to wait at least 50ms, only waited %v", elapsed)
}
// Drain the channel
<-ch
}
func TestPubSub_PublishWithContext(t *testing.T) {
ps := NewPubSub[string, string](10)
// Add a subscriber with a channel
ch, sub := ps.SubscribeByChan("test-ns", 1)
defer sub.Unsubscribe()
// Create a context
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
// Publish with context
subs, receivers, err := ps.PublishWithContext(ctx, "test-ns", "hello")
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if subs != 1 {
t.Fatalf("Expected 1 subscriber, got %d", subs)
}
if receivers != 1 {
t.Fatalf("Expected 1 receiver, got %d", receivers)
}
// Verify the message was received
select {
case msg := <-ch:
if msg != "hello" {
t.Fatalf("Expected to receive 'hello', got '%s'", msg)
}
case <-time.After(time.Second):
t.Fatal("Timed out waiting for message")
}
// Fill the channel
ps.Publish("test-ns", "fill")
// Test context cancellation with a full channel
ctx, cancel = context.WithCancel(context.Background())
// Cancel the context after a short delay
go func() {
time.Sleep(50 * time.Millisecond)
cancel()
}()
start := time.Now()
subs, receivers, err = ps.PublishWithContext(ctx, "test-ns", "context-test")
elapsed := time.Since(start)
if subs != 1 {
t.Fatalf("Expected 1 subscriber, got %d", subs)
}
// Should get a context canceled error
if err == nil {
t.Fatal("Expected context canceled error, got nil")
}
if elapsed < 50*time.Millisecond {
t.Fatalf("Expected to wait at least 50ms, only waited %v", elapsed)
}
// Drain the channel
<-ch
}
func TestPubSub_Unsubscribe(t *testing.T) {
ps := NewPubSub[string, string](10)
// Add a subscriber
ch, sub := ps.SubscribeByChan("test-ns", 1)
// Publish a message
subs, receivers := ps.Publish("test-ns", "hello")
if subs != 1 {
t.Fatalf("Expected 1 subscriber, got %d", subs)
}
if receivers != 1 {
t.Fatalf("Expected 1 receiver, got %d", receivers)
}
// Verify the message was received
select {
case msg := <-ch:
if msg != "hello" {
t.Fatalf("Expected to receive 'hello', got '%s'", msg)
}
case <-time.After(time.Second):
t.Fatal("Timed out waiting for message")
}
// Unsubscribe
sub.Unsubscribe()
// Publish again
subs, receivers = ps.Publish("test-ns", "after-unsub")
if subs != 0 {
t.Fatalf("Expected 0 subscribers after unsubscribe, got %d", subs)
}
if receivers != 0 {
t.Fatalf("Expected 0 receivers after unsubscribe, got %d", receivers)
}
// Check that the namespace is removed
namespaces := ps.Namespaces()
if len(namespaces) != 0 {
t.Fatalf("Expected 0 namespaces after unsubscribe, got %d", len(namespaces))
}
}
func TestPubSub_MultipleSubscribers(t *testing.T) {
ps := NewPubSub[string, string](10)
// Add multiple subscribers
ch1, sub1 := ps.SubscribeByChan("test-ns", 1)
defer sub1.Unsubscribe()
ch2, sub2 := ps.SubscribeByChan("test-ns", 1)
defer sub2.Unsubscribe()
var received string
var wg sync.WaitGroup
wg.Add(1)
sub3 := ps.SubscribeByCallback("test-ns", func(msg string) {
received = msg
wg.Done()
})
defer sub3.Unsubscribe()
// Publish a message
subs, receivers := ps.Publish("test-ns", "hello")
if subs != 3 {
t.Fatalf("Expected 3 subscribers, got %d", subs)
}
if receivers != 3 {
t.Fatalf("Expected 3 receivers, got %d", receivers)
}
// Verify the message was received by all subscribers
select {
case msg := <-ch1:
if msg != "hello" {
t.Fatalf("Expected ch1 to receive 'hello', got '%s'", msg)
}
case <-time.After(time.Second):
t.Fatal("Timed out waiting for message on ch1")
}
select {
case msg := <-ch2:
if msg != "hello" {
t.Fatalf("Expected ch2 to receive 'hello', got '%s'", msg)
}
case <-time.After(time.Second):
t.Fatal("Timed out waiting for message on ch2")
}
// Wait for the callback
wg.Wait()
if received != "hello" {
t.Fatalf("Expected callback to receive 'hello', got '%s'", received)
}
}

View File

@@ -5,17 +5,18 @@ import (
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"runtime/debug"
"strings"
"time"
"git.blackforestbytes.com/BlackForestBytes/goext/dataext"
"git.blackforestbytes.com/BlackForestBytes/goext/enums"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog"
"go.mongodb.org/mongo-driver/bson/primitive"
"net/http"
"os"
"runtime/debug"
"strings"
"time"
)
//
@@ -430,12 +431,22 @@ func (b *Builder) BuildAsExerr(ctxs ...context.Context) *ExErr {
return FromError(b.wrappedErr)
}
if pkgconfig.ZeroLogErrTraces && !b.noLog && (b.errorData.Severity == SevErr || b.errorData.Severity == SevFatal) {
b.errorData.ShortLog(pkgconfig.ZeroLogger.Error())
} else if pkgconfig.ZeroLogAllTraces && !b.noLog {
b.errorData.ShortLog(pkgconfig.ZeroLogger.Error())
}
if pkgconfig.ZeroLogErrTraces && !b.noLog {
if b.errorData.Severity == SevErr || b.errorData.Severity == SevFatal {
b.errorData.ShortLog(pkgconfig.ZeroLogger.Error())
} else if b.errorData.Severity == SevWarn {
b.errorData.ShortLog(pkgconfig.ZeroLogger.Warn())
} else if b.errorData.Severity == SevInfo {
b.errorData.ShortLog(pkgconfig.ZeroLogger.Info())
} else if b.errorData.Severity == SevDebug {
b.errorData.ShortLog(pkgconfig.ZeroLogger.Debug())
} else if b.errorData.Severity == SevTrace {
b.errorData.ShortLog(pkgconfig.ZeroLogger.Trace())
} else {
b.errorData.ShortLog(pkgconfig.ZeroLogger.Error()) // ?!? unknown severity
}
}
b.errorData.CallListener(MethodBuild, ListenerOpt{NoLog: b.noLog})
return b.errorData
@@ -456,10 +467,19 @@ func (b *Builder) Output(ctx context.Context, g *gin.Context) {
// this is only here to add one level to the trace
// so that .Build() and .Output() and .Print() have the same depth and our stack-skip logger can have the same skip-count
b.doOutput(ctx, g)
b.doGinOutput(ctx, g)
}
func (b *Builder) doOutput(ctx context.Context, g *gin.Context) {
// OutputRaw works teh same as Output() - but does not depend on gin and works with a raw http.ResponseWriter
func (b *Builder) OutputRaw(w http.ResponseWriter) {
warnOnPkgConfigNotInitialized()
// this is only here to add one level to the trace
// so that .Build() and .Output() and .Print() have the same depth and our stack-skip logger can have the same skip-count
b.doRawOutput(w)
}
func (b *Builder) doGinOutput(ctx context.Context, g *gin.Context) {
b.errorData.Output(g)
if (b.errorData.Severity == SevErr || b.errorData.Severity == SevFatal) && (pkgconfig.ZeroLogErrGinOutput || pkgconfig.ZeroLogAllGinOutput) {
@@ -471,6 +491,18 @@ func (b *Builder) doOutput(ctx context.Context, g *gin.Context) {
b.errorData.CallListener(MethodOutput, ListenerOpt{NoLog: b.noLog})
}
func (b *Builder) doRawOutput(w http.ResponseWriter) {
b.errorData.OutputRaw(w)
if (b.errorData.Severity == SevErr || b.errorData.Severity == SevFatal) && (pkgconfig.ZeroLogErrGinOutput || pkgconfig.ZeroLogAllGinOutput) {
b.errorData.Log(pkgconfig.ZeroLogger.Error())
} else if (b.errorData.Severity == SevWarn) && (pkgconfig.ZeroLogAllGinOutput) {
b.errorData.Log(pkgconfig.ZeroLogger.Warn())
}
b.errorData.CallListener(MethodOutput, ListenerOpt{NoLog: b.noLog})
}
// Print prints the error
// If the error is SevErr we also send it to the error-service
func (b *Builder) Print(ctxs ...context.Context) Proxy {
@@ -492,8 +524,12 @@ func (b *Builder) doPrint() Proxy {
b.errorData.ShortLog(pkgconfig.ZeroLogger.Warn())
} else if b.errorData.Severity == SevInfo {
b.errorData.ShortLog(pkgconfig.ZeroLogger.Info())
} else {
} else if b.errorData.Severity == SevDebug {
b.errorData.ShortLog(pkgconfig.ZeroLogger.Debug())
} else if b.errorData.Severity == SevTrace {
b.errorData.ShortLog(pkgconfig.ZeroLogger.Trace())
} else {
b.errorData.Log(pkgconfig.ZeroLogger.Error()) // ?!? unknown severity
}
b.errorData.CallListener(MethodPrint, ListenerOpt{NoLog: b.noLog})

View File

@@ -1,9 +1,9 @@
package exerr
import (
"github.com/gin-gonic/gin"
json "git.blackforestbytes.com/BlackForestBytes/goext/gojson"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
"github.com/gin-gonic/gin"
"net/http"
"time"
)
@@ -68,7 +68,6 @@ func (ee *ExErr) ToDefaultAPIJson() (string, error) {
gjr := json.GoJsonRender{Data: ee.ToAPIJson(true, pkgconfig.ExtendedGinOutput, pkgconfig.IncludeMetaInGinOutput), NilSafeSlices: true, NilSafeMaps: true}
r, err := gjr.RenderString()
if err != nil {
return "", err
}
@@ -143,3 +142,34 @@ func (ee *ExErr) Output(g *gin.Context) {
g.Render(statuscode, json.GoJsonRender{Data: ginOutput, NilSafeSlices: true, NilSafeMaps: true})
}
func (ee *ExErr) OutputRaw(w http.ResponseWriter) {
warnOnPkgConfigNotInitialized()
var statuscode = http.StatusInternalServerError
var baseCat = ee.RecursiveCategory()
var baseType = ee.RecursiveType()
var baseStatuscode = ee.RecursiveStatuscode()
if baseCat == CatUser {
statuscode = http.StatusBadRequest
} else if baseCat == CatSystem {
statuscode = http.StatusInternalServerError
}
if baseStatuscode != nil {
statuscode = *ee.StatusCode
} else if baseType.DefaultStatusCode != nil {
statuscode = *baseType.DefaultStatusCode
}
ginOutput, err := ee.ToDefaultAPIJson()
if err != nil {
panic(err) // cannot happen
}
w.WriteHeader(statuscode)
_, _ = w.Write([]byte(ginOutput))
}

View File

@@ -2,8 +2,9 @@ package ginext
import (
"fmt"
"github.com/gin-gonic/gin"
"git.blackforestbytes.com/BlackForestBytes/goext/exerr"
"github.com/gin-gonic/gin"
"net/http"
)
type WHandlerFunc func(PreContext) HTTPResponse
@@ -58,3 +59,31 @@ func Wrap(w *GinWrapper, fn WHandlerFunc) gin.HandlerFunc {
}
}
}
func WrapHTTPHandler(w *GinWrapper, fn http.Handler) gin.HandlerFunc {
return func(g *gin.Context) {
for _, lstr := range w.listenerBeforeRequest {
lstr(g)
}
fn.ServeHTTP(g.Writer, g.Request)
for _, lstr := range w.listenerAfterRequest {
lstr(g, nil)
}
}
}
func WrapHTTPHandlerFunc(w *GinWrapper, fn http.HandlerFunc) gin.HandlerFunc {
return func(g *gin.Context) {
for _, lstr := range w.listenerBeforeRequest {
lstr(g)
}
fn(g.Writer, g.Request)
for _, lstr := range w.listenerAfterRequest {
lstr(g, nil)
}
}
}

View File

@@ -1,8 +1,8 @@
package ginext
import (
"github.com/gin-gonic/gin"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
"github.com/gin-gonic/gin"
"net/http"
"path"
"reflect"
@@ -148,6 +148,68 @@ func (w *GinRouteBuilder) Handle(handler WHandlerFunc) {
})
}
func (w *GinRouteBuilder) HandleRawHTTPHandler(f http.Handler) {
if w.routes.wrapper.bufferBody {
arr := make([]gin.HandlerFunc, 0, len(w.handlers)+1)
arr = append(arr, BodyBuffer)
arr = append(arr, w.handlers...)
w.handlers = arr
}
middlewareNames := langext.ArrMap(w.handlers, func(v gin.HandlerFunc) string { return nameOfFunction(v) })
w.handlers = append(w.handlers, WrapHTTPHandler(w.routes.wrapper, f))
methodName := w.method
if w.method == "*" {
methodName = "ANY"
for _, method := range anyMethods {
w.routes.routes.Handle(method, w.relPath, w.handlers...)
}
} else {
w.routes.routes.Handle(w.method, w.relPath, w.handlers...)
}
w.routes.wrapper.routeSpecs = append(w.routes.wrapper.routeSpecs, ginRouteSpec{
Method: methodName,
URL: w.absPath,
Middlewares: middlewareNames,
Handler: "[HTTPHandler]",
})
}
func (w *GinRouteBuilder) HandleRawHTTPHandlerFunc(f http.HandlerFunc) {
if w.routes.wrapper.bufferBody {
arr := make([]gin.HandlerFunc, 0, len(w.handlers)+1)
arr = append(arr, BodyBuffer)
arr = append(arr, w.handlers...)
w.handlers = arr
}
middlewareNames := langext.ArrMap(w.handlers, func(v gin.HandlerFunc) string { return nameOfFunction(v) })
w.handlers = append(w.handlers, WrapHTTPHandlerFunc(w.routes.wrapper, f))
methodName := w.method
if w.method == "*" {
methodName = "ANY"
for _, method := range anyMethods {
w.routes.routes.Handle(method, w.relPath, w.handlers...)
}
} else {
w.routes.routes.Handle(w.method, w.relPath, w.handlers...)
}
w.routes.wrapper.routeSpecs = append(w.routes.wrapper.routeSpecs, ginRouteSpec{
Method: methodName,
URL: w.absPath,
Middlewares: middlewareNames,
Handler: "[HTTPHandlerFunc]",
})
}
func (w *GinWrapper) NoRoute(handler WHandlerFunc) {
handlers := make([]gin.HandlerFunc, 0)

28
go.mod
View File

@@ -11,34 +11,35 @@ require (
github.com/rs/xid v1.6.0
github.com/rs/zerolog v1.34.0
go.mongodb.org/mongo-driver v1.17.4
golang.org/x/crypto v0.39.0
golang.org/x/sys v0.33.0
golang.org/x/term v0.32.0
golang.org/x/crypto v0.41.0
golang.org/x/sys v0.35.0
golang.org/x/term v0.34.0
)
require (
github.com/disintegration/imaging v1.6.2
github.com/jung-kurt/gofpdf v1.16.2
golang.org/x/sync v0.15.0
golang.org/x/net v0.43.0
golang.org/x/sync v0.16.0
)
require (
github.com/bytedance/sonic v1.13.3 // indirect
github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/bytedance/sonic v1.14.0 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.26.0 // indirect
github.com/go-playground/validator/v10 v10.27.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/golang/snappy v1.0.0 // indirect
github.com/google/uuid v1.5.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.11 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
@@ -53,11 +54,10 @@ require (
github.com/xdg-go/scram v1.1.2 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
golang.org/x/arch v0.18.0 // indirect
golang.org/x/image v0.28.0 // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/text v0.26.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
golang.org/x/arch v0.20.0 // indirect
golang.org/x/image v0.30.0 // indirect
golang.org/x/text v0.28.0 // indirect
google.golang.org/protobuf v1.36.7 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.37.6 // indirect
modernc.org/mathutil v1.6.0 // indirect

42
go.sum
View File

@@ -23,6 +23,8 @@ github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqp
github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0=
github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP1iU4kWM=
github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
@@ -34,10 +36,14 @@ github.com/bytedance/sonic/loader v0.2.3 h1:yctD0Q3v2NOGfSWPLPvG2ggA2kV6TS6s4wio
github.com/bytedance/sonic/loader v0.2.3/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
@@ -86,6 +92,8 @@ github.com/go-playground/validator/v10 v10.25.0 h1:5Dh7cjvzR7BRZadnsVOzPhWsrwUr0
github.com/go-playground/validator/v10 v10.25.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus=
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
@@ -128,6 +136,8 @@ github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/klauspost/cpuid/v2 v2.2.11 h1:0OwqZRYI2rFrjS4kvkDnqJkKHdHaRnCm68/DY4OxRzU=
github.com/klauspost/cpuid/v2 v2.2.11/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
@@ -224,6 +234,10 @@ golang.org/x/arch v0.17.0 h1:4O3dfLzd+lQewptAHqjewQZQDyEdejz3VwgeYwkZneU=
golang.org/x/arch v0.17.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc=
golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/arch v0.19.0 h1:LmbDQUodHThXE+htjrnmVD73M//D9GTH6wFZjyDkjyU=
golang.org/x/arch v0.19.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
@@ -248,6 +262,10 @@ golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.21.0 h1:c5qV36ajHpdj4Qi0GnE0jUc/yuo33OLFaa0d+crTD5s=
@@ -266,6 +284,10 @@ golang.org/x/image v0.27.0 h1:C8gA4oWU/tKkdCfYT6T2u4faJu3MeNS5O8UPWlPF61w=
golang.org/x/image v0.27.0/go.mod h1:xbdrClrAUway1MUTEZDq9mz/UpRwYAkFFNUslZtcB+g=
golang.org/x/image v0.28.0 h1:gdem5JW1OLS4FbkWgLO+7ZeFzYtL3xClb97GaUzYMFE=
golang.org/x/image v0.28.0/go.mod h1:GUJYXtnGKEUgggyzh+Vxt+AviiCcyiwpsl8iQ8MvwGY=
golang.org/x/image v0.29.0 h1:HcdsyR4Gsuys/Axh0rDEmlBmB68rW1U9BUdB3UVHsas=
golang.org/x/image v0.29.0/go.mod h1:RVJROnf3SLK8d26OW91j4FrIHGbsJ8QnbEocVTOWQDA=
golang.org/x/image v0.30.0 h1:jD5RhkmVAnjqaCUXfbGBrn3lpxbknfN9w2UhHHU+5B4=
golang.org/x/image v0.30.0/go.mod h1:SAEUTxCCMWSrJcCy/4HwavEsfZZJlYxeHLc6tTiAe/c=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
@@ -292,6 +314,10 @@ golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
@@ -310,6 +336,8 @@ golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -335,6 +363,10 @@ golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
@@ -353,6 +385,10 @@ golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg=
golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0=
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
@@ -373,6 +409,10 @@ golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
@@ -393,6 +433,8 @@ google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwl
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A=
google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -1,5 +1,5 @@
package goext
const GoextVersion = "0.0.584"
const GoextVersion = "0.0.595"
const GoextVersionTimestamp = "2025-06-26T16:48:07+0200"
const GoextVersionTimestamp = "2025-08-20T13:03:21+0200"

View File

@@ -10,7 +10,8 @@ type RFCTime interface {
After(u AnyTime) bool
Before(u AnyTime) bool
Equal(u AnyTime) bool
EqualAny(u AnyTime) bool
Sub(u AnyTime) time.Duration
}
@@ -49,45 +50,15 @@ type AnyTime interface {
}
type RFCDuration 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 AnyTime) bool
Before(u AnyTime) bool
Equal(u AnyTime) 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 AnyTime) time.Duration
Unix() int64
UnixMilli() int64
UnixMicro() int64
UnixNano() int64
Format(layout string) string
GoString() string
Hours() float64
Minutes() float64
Seconds() float64
Microseconds() int64
Milliseconds() int64
Nanoseconds() int64
String() string
Duration() time.Duration
}
func tt(v AnyTime) time.Time {

View File

@@ -4,11 +4,11 @@ import (
"encoding/json"
"errors"
"fmt"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/bsoncodec"
"go.mongodb.org/mongo-driver/bson/bsonrw"
"go.mongodb.org/mongo-driver/bson/bsontype"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
"reflect"
"time"
)
@@ -142,7 +142,14 @@ func (t RFC3339Time) Before(u AnyTime) bool {
return t.Time().Before(tt(u))
}
func (t RFC3339Time) Equal(u AnyTime) bool {
func (t RFC3339Time) Equal(u RFC3339Time) bool {
return t.Time().Equal(u.Time())
}
func (t RFC3339Time) EqualAny(u AnyTime) bool {
if u == nil {
return false
}
return t.Time().Equal(tt(u))
}

View File

@@ -4,11 +4,11 @@ import (
"encoding/json"
"errors"
"fmt"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/bsoncodec"
"go.mongodb.org/mongo-driver/bson/bsonrw"
"go.mongodb.org/mongo-driver/bson/bsontype"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
"reflect"
"time"
)
@@ -142,7 +142,14 @@ func (t RFC3339NanoTime) Before(u AnyTime) bool {
return t.Time().Before(tt(u))
}
func (t RFC3339NanoTime) Equal(u AnyTime) bool {
func (t RFC3339NanoTime) Equal(u RFC3339NanoTime) bool {
return t.Time().Equal(u.Time())
}
func (t RFC3339NanoTime) EqualAny(u AnyTime) bool {
if u == nil {
return false
}
return t.Time().Equal(tt(u))
}

View File

@@ -41,7 +41,7 @@ func TestRoundtrip(t *testing.T) {
tst.AssertEqual(t, string(jstr1), string(jstr2))
if !w1.Value.Equal(&w2.Value) {
if !w1.Value.EqualAny(&w2.Value) {
t.Errorf("time differs")
}

View File

@@ -4,11 +4,11 @@ import (
"encoding/json"
"errors"
"fmt"
"git.blackforestbytes.com/BlackForestBytes/goext/timeext"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/bsoncodec"
"go.mongodb.org/mongo-driver/bson/bsonrw"
"go.mongodb.org/mongo-driver/bson/bsontype"
"git.blackforestbytes.com/BlackForestBytes/goext/timeext"
"reflect"
"time"
)

View File

@@ -4,11 +4,11 @@ import (
"encoding/json"
"errors"
"fmt"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/bsoncodec"
"go.mongodb.org/mongo-driver/bson/bsonrw"
"go.mongodb.org/mongo-driver/bson/bsontype"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
"reflect"
"strconv"
"time"
@@ -136,7 +136,14 @@ func (t UnixTime) Before(u AnyTime) bool {
return t.Time().Before(tt(u))
}
func (t UnixTime) Equal(u AnyTime) bool {
func (t UnixTime) Equal(u UnixTime) bool {
return t.Time().Equal(u.Time())
}
func (t UnixTime) EqualAny(u AnyTime) bool {
if u == nil {
return false
}
return t.Time().Equal(tt(u))
}

View File

@@ -4,11 +4,11 @@ import (
"encoding/json"
"errors"
"fmt"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/bsoncodec"
"go.mongodb.org/mongo-driver/bson/bsonrw"
"go.mongodb.org/mongo-driver/bson/bsontype"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
"reflect"
"strconv"
"time"
@@ -136,7 +136,14 @@ func (t UnixMilliTime) Before(u AnyTime) bool {
return t.Time().Before(tt(u))
}
func (t UnixMilliTime) Equal(u AnyTime) bool {
func (t UnixMilliTime) Equal(u UnixMilliTime) bool {
return t.Time().Equal(u.Time())
}
func (t UnixMilliTime) EqualAny(u AnyTime) bool {
if u == nil {
return false
}
return t.Time().Equal(tt(u))
}

View File

@@ -4,11 +4,11 @@ import (
"encoding/json"
"errors"
"fmt"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/bsoncodec"
"go.mongodb.org/mongo-driver/bson/bsonrw"
"go.mongodb.org/mongo-driver/bson/bsontype"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
"reflect"
"strconv"
"time"
@@ -136,7 +136,14 @@ func (t UnixNanoTime) Before(u AnyTime) bool {
return t.Time().Before(tt(u))
}
func (t UnixNanoTime) Equal(u AnyTime) bool {
func (t UnixNanoTime) Equal(u UnixNanoTime) bool {
return t.Time().Equal(u.Time())
}
func (t UnixNanoTime) EqualAny(u AnyTime) bool {
if u == nil {
return false
}
return t.Time().Equal(tt(u))
}

View File

@@ -1,6 +1,7 @@
package syncext
import (
"golang.org/x/net/context"
"time"
)
@@ -26,6 +27,15 @@ func WriteChannelWithTimeout[T any](c chan T, msg T, timeout time.Duration) bool
}
}
func WriteChannelWithContext[T any](ctx context.Context, c chan T, msg T) error {
select {
case c <- msg:
return nil
case <-ctx.Done():
return ctx.Err()
}
}
func ReadNonBlocking[T any](c chan T) (T, bool) {
select {
case msg := <-c:

View File

@@ -3,6 +3,7 @@ package timeext
import (
"fmt"
"math"
"sort"
"time"
)
@@ -142,6 +143,42 @@ func Max(a time.Time, b time.Time) time.Time {
}
}
func Avg(v ...time.Time) time.Time {
if len(v) == 0 {
return time.Time{}
}
t0 := v[0].UnixNano()
var dsum int64
for _, t := range v {
dsum += t.UnixNano() - t0
}
tAvg := t0 + (dsum / int64(len(v)))
return time.Unix(0, tAvg)
}
func Median(v ...time.Time) time.Time {
if len(v) == 0 {
return time.Time{}
}
sorted := make([]time.Time, len(v))
copy(sorted, v)
sort.Slice(sorted, func(i, j int) bool {
return sorted[i].UnixNano() < sorted[j].UnixNano()
})
mid := len(sorted) / 2
if len(sorted)%2 == 0 {
return Avg(sorted[mid-1], sorted[mid])
} else {
return sorted[mid]
}
}
func UnixFloatSeconds(v float64) time.Time {
sec, dec := math.Modf(v)
return time.Unix(int64(sec), int64(dec*(1e9)))

View File

@@ -227,3 +227,192 @@ func TestDaysInMonth_FebruaryNonLeapYear(t *testing.T) {
t.Errorf("Expected %d but got %d", expected, result)
}
}
func TestAvg_MultipleValues(t *testing.T) {
t1 := time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC)
t2 := time.Date(2022, 1, 3, 0, 0, 0, 0, time.UTC)
t3 := time.Date(2022, 1, 5, 0, 0, 0, 0, time.UTC)
// Average should be January 3, 2022 (middle date)
expected := time.Date(2022, 1, 3, 0, 0, 0, 0, time.UTC)
result := Avg(t1, t2, t3)
if !result.Equal(expected) {
t.Errorf("Expected %v but got %v", expected, result)
}
}
func TestAvg_ManyValues(t *testing.T) {
t1 := time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC)
t2 := time.Date(2022, 1, 2, 0, 0, 0, 0, time.UTC)
t3 := time.Date(2022, 1, 3, 0, 0, 0, 0, time.UTC)
t4 := time.Date(2022, 1, 4, 0, 0, 0, 0, time.UTC)
t5 := time.Date(2022, 1, 5, 0, 0, 0, 0, time.UTC)
t6 := time.Date(2022, 1, 6, 0, 0, 0, 0, time.UTC)
t7 := time.Date(2022, 1, 7, 0, 0, 0, 0, time.UTC)
t8 := time.Date(2022, 1, 8, 0, 0, 0, 0, time.UTC)
t9 := time.Date(2022, 1, 9, 0, 0, 0, 0, time.UTC)
expected := time.Date(2022, 1, 5, 0, 0, 0, 0, time.UTC)
result := Avg(t1, t2, t3, t4, t5, t6, t7, t8, t9)
if !result.Equal(expected) {
t.Errorf("Expected %v but got %v", expected, result)
}
}
func TestAvg_TwoValues(t *testing.T) {
t1 := time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC)
t2 := time.Date(2022, 1, 3, 0, 0, 0, 0, time.UTC)
expected := time.Date(2022, 1, 2, 0, 0, 0, 0, time.UTC)
result := Avg(t1, t2)
if !result.Equal(expected) {
t.Errorf("Expected %v but got %v", expected, result)
}
}
func TestAvg_EmptySlice(t *testing.T) {
result := Avg()
expected := time.Time{}
if !result.Equal(expected) {
t.Errorf("Expected zero time but got %v", result)
}
}
func TestMedian_OddNumberOfValues(t *testing.T) {
t1 := time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC)
t2 := time.Date(2022, 1, 3, 0, 0, 0, 0, time.UTC)
t3 := time.Date(2022, 1, 5, 0, 0, 0, 0, time.UTC)
// Median should be the middle date
expected := t2
result := Median(t1, t2, t3)
if !result.Equal(expected) {
t.Errorf("Expected %v but got %v", expected, result)
}
}
func TestMedian_EvenNumberOfValues(t *testing.T) {
t1 := time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC)
t2 := time.Date(2022, 1, 2, 0, 0, 0, 0, time.UTC)
t3 := time.Date(2022, 1, 3, 0, 0, 0, 0, time.UTC)
t4 := time.Date(2022, 1, 4, 0, 0, 0, 0, time.UTC)
// Median for even number of values should be average of middle two
expected := time.Date(2022, 1, 2, 12, 0, 0, 0, time.UTC)
result := Median(t1, t2, t3, t4)
if !result.Equal(expected) {
t.Errorf("Expected %v but got %v", expected, result)
}
}
func TestMedian_UnsortedValues(t *testing.T) {
t1 := time.Date(2022, 1, 5, 0, 0, 0, 0, time.UTC)
t2 := time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC)
t3 := time.Date(2022, 1, 3, 0, 0, 0, 0, time.UTC)
// Median should correctly sort values first
expected := t3
result := Median(t1, t2, t3)
if !result.Equal(expected) {
t.Errorf("Expected %v but got %v", expected, result)
}
}
func TestMedian_EmptySlice(t *testing.T) {
result := Median()
expected := time.Time{}
if !result.Equal(expected) {
t.Errorf("Expected zero time but got %v", result)
}
}
func TestTimeToDatePart(t *testing.T) {
tz := TimezoneBerlin
tm := time.Date(2022, 1, 1, 13, 14, 15, 0, tz)
expected := time.Date(2022, 1, 1, 0, 0, 0, 0, tz)
result := TimeToDatePart(tm, tz)
if !result.Equal(expected) {
t.Errorf("Expected %v but got %v", expected, result)
}
}
func TestTimeToWeekStart(t *testing.T) {
tz := TimezoneBerlin
// January 5, 2022 was a Wednesday
tm := time.Date(2022, 1, 5, 13, 14, 15, 0, tz)
// Should return Monday, January 3, 2022
expected := time.Date(2022, 1, 3, 0, 0, 0, 0, tz)
result := TimeToWeekStart(tm, tz)
if !result.Equal(expected) {
t.Errorf("Expected %v but got %v", expected, result)
}
}
func TestTimeToWeekStart_WhenMonday(t *testing.T) {
tz := TimezoneBerlin
// January 3, 2022 was a Monday
tm := time.Date(2022, 1, 3, 13, 14, 15, 0, tz)
expected := time.Date(2022, 1, 3, 0, 0, 0, 0, tz)
result := TimeToWeekStart(tm, tz)
if !result.Equal(expected) {
t.Errorf("Expected %v but got %v", expected, result)
}
}
func TestTimeToMonthStart(t *testing.T) {
tz := TimezoneBerlin
tm := time.Date(2022, 1, 15, 13, 14, 15, 0, tz)
expected := time.Date(2022, 1, 1, 0, 0, 0, 0, tz)
result := TimeToMonthStart(tm, tz)
if !result.Equal(expected) {
t.Errorf("Expected %v but got %v", expected, result)
}
}
func TestTimeToMonthEnd(t *testing.T) {
tz := TimezoneBerlin
tm := time.Date(2022, 1, 15, 13, 14, 15, 0, tz)
expected := time.Date(2022, 2, 1, 0, 0, 0, 0, tz).Add(-1)
result := TimeToMonthEnd(tm, tz)
if !result.Equal(expected) {
t.Errorf("Expected %v but got %v", expected, result)
}
}
func TestTimeToYearStart(t *testing.T) {
tz := TimezoneBerlin
tm := time.Date(2022, 5, 15, 13, 14, 15, 0, tz)
expected := time.Date(2022, 1, 1, 0, 0, 0, 0, tz)
result := TimeToYearStart(tm, tz)
if !result.Equal(expected) {
t.Errorf("Expected %v but got %v", expected, result)
}
}
func TestTimeToYearEnd(t *testing.T) {
tz := TimezoneBerlin
tm := time.Date(2022, 5, 15, 13, 14, 15, 0, tz)
expected := time.Date(2023, 1, 1, 0, 0, 0, 0, tz).Add(-1)
result := TimeToYearEnd(tm, tz)
if !result.Equal(expected) {
t.Errorf("Expected %v but got %v", expected, result)
}
}
func TestTimeToNextYearStart(t *testing.T) {
tz := TimezoneBerlin
tm := time.Date(2022, 5, 15, 13, 14, 15, 0, tz)
expected := time.Date(2023, 1, 1, 0, 0, 0, 0, tz)
result := TimeToNextYearStart(tm, tz)
if !result.Equal(expected) {
t.Errorf("Expected %v but got %v", expected, result)
}
}