mirror of
https://github.com/yusing/godoxy.git
synced 2026-03-29 21:31:48 +02:00
refactor: move some utility functions to goutils and update references
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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()))
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
//go:build !debug
|
||||
|
||||
package pool
|
||||
|
||||
func (p Pool[T]) checkExists(key string) {
|
||||
// no-op in production
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -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
|
||||
*/
|
||||
Reference in New Issue
Block a user