refactor: move some utility functions to goutils and update references

This commit is contained in:
yusing
2025-12-04 12:17:33 +08:00
parent ae5063b705
commit de15dbf405
27 changed files with 41 additions and 430 deletions

View File

@@ -1,36 +0,0 @@
package utils
import (
"fmt"
"os"
"path"
)
// Recursively lists all files in a directory until `maxDepth` is reached
// Returns a slice of file paths relative to `dir`.
func ListFiles(dir string, maxDepth int, hideHidden ...bool) ([]string, error) {
entries, err := os.ReadDir(dir)
if err != nil {
return nil, fmt.Errorf("error listing directory %s: %w", dir, err)
}
hideHiddenFiles := len(hideHidden) > 0 && hideHidden[0]
files := make([]string, 0)
for _, entry := range entries {
if hideHiddenFiles && entry.Name()[0] == '.' {
continue
}
if entry.IsDir() {
if maxDepth <= 0 {
continue
}
subEntries, err := ListFiles(path.Join(dir, entry.Name()), maxDepth-1)
if err != nil {
return nil, err
}
files = append(files, subEntries...)
} else {
files = append(files, path.Join(dir, entry.Name()))
}
}
return files, nil
}

View File

@@ -1,50 +0,0 @@
package utils
import (
"reflect"
strutils "github.com/yusing/goutils/strings"
)
func NearestField(input string, s any) string {
minDistance := -1
nearestField := ""
var fields []string
switch s := s.(type) {
case []string:
fields = s
default:
t := reflect.TypeOf(s)
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
switch t.Kind() {
case reflect.Struct:
fields = make([]string, 0)
for i := range t.NumField() {
jsonTag, ok := t.Field(i).Tag.Lookup("json")
if ok {
fields = append(fields, jsonTag)
} else {
fields = append(fields, t.Field(i).Name)
}
}
case reflect.Map:
keys := reflect.ValueOf(s).MapKeys()
fields = make([]string, len(keys))
for i, key := range keys {
fields[i] = key.String()
}
default:
panic("NearestField unsupported type: " + t.String())
}
}
for _, field := range fields {
distance := strutils.LevenshteinDistance(input, field)
if minDistance == -1 || distance < minDistance {
minDistance = distance
nearestField = field
}
}
return nearestField
}

View File

@@ -1,123 +0,0 @@
package pool
import (
"sort"
"sync/atomic"
"github.com/puzpuzpuz/xsync/v4"
"github.com/rs/zerolog/log"
)
type (
Pool[T Object] struct {
m *xsync.Map[string, T]
name string
disableLog atomic.Bool
}
// Preferable allows an object to express deterministic replacement preference
// when multiple objects with the same key are added to the pool.
// If new.PreferOver(old) returns true, the new object replaces the old one.
Preferable interface {
PreferOver(other any) bool
}
Object interface {
Key() string
Name() string
}
ObjectWithDisplayName interface {
Object
DisplayName() string
}
)
func New[T Object](name string) Pool[T] {
return Pool[T]{m: xsync.NewMap[string, T](), name: name}
}
func (p *Pool[T]) ToggleLog(v bool) {
p.disableLog.Store(v)
}
func (p *Pool[T]) Name() string {
return p.name
}
func (p *Pool[T]) Add(obj T) {
p.AddKey(obj.Key(), obj)
}
func (p *Pool[T]) AddKey(key string, obj T) {
if cur, exists := p.m.Load(key); exists {
if newPref, ok := any(obj).(Preferable); ok {
if !newPref.PreferOver(cur) {
// keep existing
return
}
}
}
p.checkExists(key)
p.m.Store(key, obj)
p.logAction("added", obj)
}
func (p *Pool[T]) AddIfNotExists(obj T) (actual T, added bool) {
actual, loaded := p.m.LoadOrStore(obj.Key(), obj)
if !loaded {
p.logAction("added", obj)
}
return actual, !loaded
}
func (p *Pool[T]) Del(obj T) {
p.m.Delete(obj.Key())
p.logAction("removed", obj)
}
func (p *Pool[T]) DelKey(key string) {
if v, exists := p.m.LoadAndDelete(key); exists {
p.logAction("removed", v)
}
}
func (p *Pool[T]) Get(key string) (T, bool) {
return p.m.Load(key)
}
func (p *Pool[T]) Size() int {
return p.m.Size()
}
func (p *Pool[T]) Clear() {
p.m.Clear()
}
func (p *Pool[T]) Iter(fn func(k string, v T) bool) {
p.m.Range(fn)
}
func (p *Pool[T]) Slice() []T {
slice := make([]T, 0, p.m.Size())
for _, v := range p.m.Range {
slice = append(slice, v)
}
sort.Slice(slice, func(i, j int) bool {
return slice[i].Name() < slice[j].Name()
})
return slice
}
func (p *Pool[T]) logAction(action string, obj T) {
if p.disableLog.Load() {
return
}
if withName, ok := any(obj).(ObjectWithDisplayName); ok {
disp, name := withName.DisplayName(), withName.Name()
if disp != name {
log.Info().Msgf("%s: %s %s (%s)", p.name, action, disp, name)
} else {
log.Info().Msgf("%s: %s %s", p.name, action, name)
}
} else {
log.Info().Msgf("%s: %s %s", p.name, action, obj.Name())
}
}

View File

@@ -1,15 +0,0 @@
//go:build debug
package pool
import (
"runtime/debug"
"github.com/rs/zerolog/log"
)
func (p Pool[T]) checkExists(key string) {
if _, ok := p.m.Load(key); ok {
log.Warn().Msgf("%s: key %s already exists\nstacktrace: %s", p.name, key, string(debug.Stack()))
}
}

View File

@@ -1,7 +0,0 @@
//go:build !debug
package pool
func (p Pool[T]) checkExists(key string) {
// no-op in production
}

View File

@@ -1,44 +0,0 @@
package utils
import (
"time"
"go.uber.org/atomic"
)
var (
TimeNow = DefaultTimeNow
shouldCallTimeNow atomic.Bool
timeNowTicker = time.NewTicker(shouldCallTimeNowInterval)
lastTimeNow = atomic.NewTime(time.Now())
)
const shouldCallTimeNowInterval = 100 * time.Millisecond
func MockTimeNow(t time.Time) {
TimeNow = func() time.Time {
return t
}
}
// DefaultTimeNow is a time.Now wrapper that reduces the number of calls to time.Now
// by caching the result and only allow calling time.Now when the ticker fires.
//
// Returned value may have +-100ms error.
func DefaultTimeNow() time.Time {
swapped := shouldCallTimeNow.CompareAndSwap(false, true)
if swapped { // first call
now := time.Now()
lastTimeNow.Store(now)
return now
}
return lastTimeNow.Load()
}
func init() {
go func() {
for range timeNowTicker.C {
shouldCallTimeNow.Store(true)
}
}()
}

View File

@@ -1,104 +0,0 @@
package utils
import (
"testing"
"time"
)
var sink time.Time
func BenchmarkTimeNow(b *testing.B) {
b.Run("default", func(b *testing.B) {
for b.Loop() {
sink = time.Now()
}
})
b.Run("reduced_call", func(b *testing.B) {
for b.Loop() {
sink = DefaultTimeNow()
}
})
}
func TestDefaultTimeNow(t *testing.T) {
// Get initial time
t1 := DefaultTimeNow()
// Second call should return the same time without calling time.Now
t2 := DefaultTimeNow()
if !t1.Equal(t2) {
t.Errorf("Expected t1 == t2, got t1 = %v, t2 = %v", t1, t2)
}
// Set shouldCallTimeNow to true
shouldCallTimeNow.Store(true)
// This should update the lastTimeNow
t3 := DefaultTimeNow()
// The time should have changed
if t2.Equal(t3) {
t.Errorf("Expected t2 != t3, got t2 = %v, t3 = %v", t2, t3)
}
// Fourth call should return the same time as third call
t4 := DefaultTimeNow()
if !t3.Equal(t4) {
t.Errorf("Expected t3 == t4, got t3 = %v, t4 = %v", t3, t4)
}
}
func TestMockTimeNow(t *testing.T) {
// Save the original TimeNow function to restore later
originalTimeNow := TimeNow
defer func() {
TimeNow = originalTimeNow
}()
// Create a fixed time
fixedTime := time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC)
// Mock the time
MockTimeNow(fixedTime)
// TimeNow should return the fixed time
result := TimeNow()
if !result.Equal(fixedTime) {
t.Errorf("Expected %v, got %v", fixedTime, result)
}
}
func TestTimeNowTicker(t *testing.T) {
// This test verifies that the ticker properly updates shouldCallTimeNow
// Reset the flag
shouldCallTimeNow.Store(false)
// Wait for the ticker to tick (slightly more than the interval)
time.Sleep(shouldCallTimeNowInterval + 10*time.Millisecond)
// The ticker should have set shouldCallTimeNow to true
if !shouldCallTimeNow.Load() {
t.Error("Expected shouldCallTimeNow to be true after ticker interval")
}
// Call DefaultTimeNow which should reset the flag
DefaultTimeNow()
// Check that the flag is reset
if shouldCallTimeNow.Load() {
t.Error("Expected shouldCallTimeNow to be false after calling DefaultTimeNow")
}
}
/*
BenchmarkTimeNow
BenchmarkTimeNow/default
BenchmarkTimeNow/default-20 48158628 24.86 ns/op 0 B/op 0 allocs/op
BenchmarkTimeNow/reduced_call
BenchmarkTimeNow/reduced_call-20 1000000000 1.000 ns/op 0 B/op 0 allocs/op
*/