From 39df26ee212466828b97a9f27e8dc8046152736d Mon Sep 17 00:00:00 2001 From: Gani Georgiev Date: Mon, 23 Dec 2024 15:44:00 +0200 Subject: [PATCH] changed store.Store to accept generic key type --- CHANGELOG.md | 3 ++ apis/middlewares_rate_limit.go | 10 +++---- core/app.go | 2 +- core/base.go | 6 ++-- core/log_printer.go | 2 +- core/record_model.go | 12 ++++---- tools/list/list.go | 2 +- tools/router/event.go | 2 +- tools/store/store.go | 54 +++++++++++++++++----------------- tools/store/store_test.go | 10 +++---- tools/subscriptions/broker.go | 4 +-- tools/template/registry.go | 4 +-- 12 files changed, 57 insertions(+), 54 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9cd4ee0b..069a7766 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,9 @@ With this change the "multi-match" operators are also normalized in case the targetted colletion doesn't have any records (_or in other words, `@collection.example.someField != "test"` will result to `true` if `example` collection has no records because it satisfies the condition that all available "example" records mustn't have `someField` equal to "test"_). +- ⚠️ Changed the type definition of `store.Store[T any]` to `store.Store[K comparable, T any]` to allow support for custom store key types. + For most users it should be non-breaking change, BUT if you are creating manually `store.New[any](nil)` instances you'll have to specify the key generic type, aka. `store.New[string, any](nil)`. + ## v0.23.12 diff --git a/apis/middlewares_rate_limit.go b/apis/middlewares_rate_limit.go index f794eb86..886503dc 100644 --- a/apis/middlewares_rate_limit.go +++ b/apis/middlewares_rate_limit.go @@ -111,7 +111,7 @@ func checkCollectionRateLimit(e *core.RequestEvent, collection *core.Collection, // //nolint:unused func isClientRateLimited(e *core.RequestEvent, rtId string) bool { - rateLimiters, ok := e.App.Store().Get(rateLimitersStoreKey).(*store.Store[*rateLimiter]) + rateLimiters, ok := e.App.Store().Get(rateLimitersStoreKey).(*store.Store[string, *rateLimiter]) if !ok || rateLimiters == nil { return false } @@ -146,7 +146,7 @@ func checkRateLimit(e *core.RequestEvent, rtId string, rule core.RateLimitRule) rateLimiters := e.App.Store().GetOrSet(rateLimitersStoreKey, func() any { return initRateLimitersStore(e.App) - }).(*store.Store[*rateLimiter]) + }).(*store.Store[string, *rateLimiter]) if rateLimiters == nil { e.App.Logger().Warn("Failed to retrieve app rate limiters store") return nil @@ -198,9 +198,9 @@ func destroyRateLimitersStore(app core.App) { app.Store().Remove(rateLimitersStoreKey) } -func initRateLimitersStore(app core.App) *store.Store[*rateLimiter] { +func initRateLimitersStore(app core.App) *store.Store[string, *rateLimiter] { app.Cron().Add(rateLimitersCronKey, "2 * * * *", func() { // offset a little since too many cleanup tasks execute at 00 - limitersStore, ok := app.Store().Get(rateLimitersStoreKey).(*store.Store[*rateLimiter]) + limitersStore, ok := app.Store().Get(rateLimitersStoreKey).(*store.Store[string, *rateLimiter]) if !ok { return } @@ -225,7 +225,7 @@ func initRateLimitersStore(app core.App) *store.Store[*rateLimiter] { }, }) - return store.New[*rateLimiter](nil) + return store.New[string, *rateLimiter](nil) } func newRateLimiter(maxAllowed int, intervalInSec int64, minDeleteIntervalInSec int64) *rateLimiter { diff --git a/core/app.go b/core/app.go index cf03d51a..08acce42 100644 --- a/core/app.go +++ b/core/app.go @@ -71,7 +71,7 @@ type App interface { Settings() *Settings // Store returns the app runtime store. - Store() *store.Store[any] + Store() *store.Store[string, any] // Cron returns the app cron instance. Cron() *cron.Cron diff --git a/core/base.go b/core/base.go index 1f62bf51..2e221f72 100644 --- a/core/base.go +++ b/core/base.go @@ -70,7 +70,7 @@ var _ App = (*BaseApp)(nil) type BaseApp struct { config *BaseAppConfig txInfo *txAppInfo - store *store.Store[any] + store *store.Store[string, any] cron *cron.Cron settings *Settings subscriptionsBroker *subscriptions.Broker @@ -194,7 +194,7 @@ type BaseApp struct { func NewBaseApp(config BaseAppConfig) *BaseApp { app := &BaseApp{ settings: newDefaultSettings(), - store: store.New[any](nil), + store: store.New[string, any](nil), cron: cron.New(), subscriptionsBroker: subscriptions.NewBroker(), config: &config, @@ -532,7 +532,7 @@ func (app *BaseApp) Settings() *Settings { } // Store returns the app runtime store. -func (app *BaseApp) Store() *store.Store[any] { +func (app *BaseApp) Store() *store.Store[string, any] { return app.store } diff --git a/core/log_printer.go b/core/log_printer.go index 77b6c16b..0f8eb3b8 100644 --- a/core/log_printer.go +++ b/core/log_printer.go @@ -11,7 +11,7 @@ import ( "github.com/spf13/cast" ) -var cachedColors = store.New[*color.Color](nil) +var cachedColors = store.New[string, *color.Color](nil) // getColor returns [color.Color] object and cache it (if not already). func getColor(attrs ...color.Attribute) (c *color.Color) { diff --git a/core/record_model.go b/core/record_model.go index 136926ad..4b85e21c 100644 --- a/core/record_model.go +++ b/core/record_model.go @@ -37,9 +37,9 @@ var ( type Record struct { collection *Collection originalData map[string]any - customVisibility *store.Store[bool] - data *store.Store[any] - expand *store.Store[any] + customVisibility *store.Store[string, bool] + data *store.Store[string, any] + expand *store.Store[string, any] BaseModel @@ -537,8 +537,8 @@ func newRecordsFromNullStringMaps(collection *Collection, rows []dbx.NullStringM func NewRecord(collection *Collection) *Record { record := &Record{ collection: collection, - data: store.New[any](nil), - customVisibility: store.New[bool](nil), + data: store.New[string, any](nil), + customVisibility: store.New[string, bool](nil), originalData: make(map[string]any, len(collection.Fields)), } @@ -681,7 +681,7 @@ func (m *Record) Expand() map[string]any { // SetExpand replaces the current Record's expand with the provided expand arg data (shallow copied). func (m *Record) SetExpand(expand map[string]any) { if m.expand == nil { - m.expand = store.New[any](nil) + m.expand = store.New[string, any](nil) } m.expand.Reset(expand) diff --git a/tools/list/list.go b/tools/list/list.go index aff00597..76c41eed 100644 --- a/tools/list/list.go +++ b/tools/list/list.go @@ -9,7 +9,7 @@ import ( "github.com/spf13/cast" ) -var cachedPatterns = store.New[*regexp.Regexp](nil) +var cachedPatterns = store.New[string, *regexp.Regexp](nil) // SubtractSlice returns a new slice with only the "base" elements // that don't exist in "subtract". diff --git a/tools/router/event.go b/tools/router/event.go index 8e8d124e..7153269f 100644 --- a/tools/router/event.go +++ b/tools/router/event.go @@ -34,7 +34,7 @@ type Event struct { hook.Event - data store.Store[any] + data store.Store[string, any] } // RWUnwrapper specifies that an http.ResponseWriter could be "unwrapped" diff --git a/tools/store/store.go b/tools/store/store.go index 9fd2096f..03525702 100644 --- a/tools/store/store.go +++ b/tools/store/store.go @@ -9,15 +9,15 @@ import ( const ShrinkThreshold = 200 // the number is arbitrary chosen // Store defines a concurrent safe in memory key-value data store. -type Store[T any] struct { - data map[string]T +type Store[K comparable, T any] struct { + data map[K]T mu sync.RWMutex deleted int64 } // New creates a new Store[T] instance with a shallow copy of the provided data (if any). -func New[T any](data map[string]T) *Store[T] { - s := &Store[T]{} +func New[K comparable, T any](data map[K]T) *Store[K, T] { + s := &Store[K, T]{} s.Reset(data) @@ -26,24 +26,24 @@ func New[T any](data map[string]T) *Store[T] { // Reset clears the store and replaces the store data with a // shallow copy of the provided newData. -func (s *Store[T]) Reset(newData map[string]T) { +func (s *Store[K, T]) Reset(newData map[K]T) { s.mu.Lock() defer s.mu.Unlock() if len(newData) > 0 { - s.data = make(map[string]T, len(newData)) + s.data = make(map[K]T, len(newData)) for k, v := range newData { s.data[k] = v } } else { - s.data = make(map[string]T) + s.data = make(map[K]T) } s.deleted = 0 } // Length returns the current number of elements in the store. -func (s *Store[T]) Length() int { +func (s *Store[K, T]) Length() int { s.mu.RLock() defer s.mu.RUnlock() @@ -51,14 +51,14 @@ func (s *Store[T]) Length() int { } // RemoveAll removes all the existing store entries. -func (s *Store[T]) RemoveAll() { +func (s *Store[K, T]) RemoveAll() { s.Reset(nil) } // Remove removes a single entry from the store. // // Remove does nothing if key doesn't exist in the store. -func (s *Store[T]) Remove(key string) { +func (s *Store[K, T]) Remove(key K) { s.mu.Lock() defer s.mu.Unlock() @@ -69,7 +69,7 @@ func (s *Store[T]) Remove(key string) { // // @todo remove after https://github.com/golang/go/issues/20135 if s.deleted >= ShrinkThreshold { - newData := make(map[string]T, len(s.data)) + newData := make(map[K]T, len(s.data)) for k, v := range s.data { newData[k] = v } @@ -79,7 +79,7 @@ func (s *Store[T]) Remove(key string) { } // Has checks if element with the specified key exist or not. -func (s *Store[T]) Has(key string) bool { +func (s *Store[K, T]) Has(key K) bool { s.mu.RLock() defer s.mu.RUnlock() @@ -91,7 +91,7 @@ func (s *Store[T]) Has(key string) bool { // Get returns a single element value from the store. // // If key is not set, the zero T value is returned. -func (s *Store[T]) Get(key string) T { +func (s *Store[K, T]) Get(key K) T { s.mu.RLock() defer s.mu.RUnlock() @@ -99,7 +99,7 @@ func (s *Store[T]) Get(key string) T { } // GetOk is similar to Get but returns also a boolean indicating whether the key exists or not. -func (s *Store[T]) GetOk(key string) (T, bool) { +func (s *Store[K, T]) GetOk(key K) (T, bool) { s.mu.RLock() defer s.mu.RUnlock() @@ -109,11 +109,11 @@ func (s *Store[T]) GetOk(key string) (T, bool) { } // GetAll returns a shallow copy of the current store data. -func (s *Store[T]) GetAll() map[string]T { +func (s *Store[K, T]) GetAll() map[K]T { s.mu.RLock() defer s.mu.RUnlock() - var clone = make(map[string]T, len(s.data)) + var clone = make(map[K]T, len(s.data)) for k, v := range s.data { clone[k] = v @@ -123,7 +123,7 @@ func (s *Store[T]) GetAll() map[string]T { } // Values returns a slice with all of the current store values. -func (s *Store[T]) Values() []T { +func (s *Store[K, T]) Values() []T { s.mu.RLock() defer s.mu.RUnlock() @@ -137,12 +137,12 @@ func (s *Store[T]) Values() []T { } // Set sets (or overwrite if already exist) a new value for key. -func (s *Store[T]) Set(key string, value T) { +func (s *Store[K, T]) Set(key K, value T) { s.mu.Lock() defer s.mu.Unlock() if s.data == nil { - s.data = make(map[string]T) + s.data = make(map[K]T) } s.data[key] = value @@ -150,7 +150,7 @@ func (s *Store[T]) Set(key string, value T) { // GetOrSet retrieves a single existing value for the provided key // or stores a new one if it doesn't exist. -func (s *Store[T]) GetOrSet(key string, setFunc func() T) T { +func (s *Store[K, T]) GetOrSet(key K, setFunc func() T) T { // lock only reads to minimize locks contention s.mu.RLock() v, ok := s.data[key] @@ -160,7 +160,7 @@ func (s *Store[T]) GetOrSet(key string, setFunc func() T) T { s.mu.Lock() v = setFunc() if s.data == nil { - s.data = make(map[string]T) + s.data = make(map[K]T) } s.data[key] = v s.mu.Unlock() @@ -174,12 +174,12 @@ func (s *Store[T]) GetOrSet(key string, setFunc func() T) T { // This method is similar to Set() but **it will skip adding new elements** // to the store if the store length has reached the specified limit. // false is returned if maxAllowedElements limit is reached. -func (s *Store[T]) SetIfLessThanLimit(key string, value T, maxAllowedElements int) bool { +func (s *Store[K, T]) SetIfLessThanLimit(key K, value T, maxAllowedElements int) bool { s.mu.Lock() defer s.mu.Unlock() if s.data == nil { - s.data = make(map[string]T) + s.data = make(map[K]T) } // check for existing item @@ -200,8 +200,8 @@ func (s *Store[T]) SetIfLessThanLimit(key string, value T, maxAllowedElements in // provided JSON data into the store. // // The store entries that match with the ones from the data will be overwritten with the new value. -func (s *Store[T]) UnmarshalJSON(data []byte) error { - raw := map[string]T{} +func (s *Store[K, T]) UnmarshalJSON(data []byte) error { + raw := map[K]T{} if err := json.Unmarshal(data, &raw); err != nil { return err } @@ -210,7 +210,7 @@ func (s *Store[T]) UnmarshalJSON(data []byte) error { defer s.mu.Unlock() if s.data == nil { - s.data = make(map[string]T) + s.data = make(map[K]T) } for k, v := range raw { @@ -222,6 +222,6 @@ func (s *Store[T]) UnmarshalJSON(data []byte) error { // MarshalJSON implements [json.Marshaler] and export the current // store data into valid JSON. -func (s *Store[T]) MarshalJSON() ([]byte, error) { +func (s *Store[K, T]) MarshalJSON() ([]byte, error) { return json.Marshal(s.GetAll()) } diff --git a/tools/store/store_test.go b/tools/store/store_test.go index 96e807a3..9da1a5a9 100644 --- a/tools/store/store_test.go +++ b/tools/store/store_test.go @@ -227,7 +227,7 @@ func TestValues(t *testing.T) { } func TestSet(t *testing.T) { - s := store.Store[int]{} + s := store.Store[string, int]{} data := map[string]int{"test1": 0, "test2": 1, "test3": 3} @@ -281,7 +281,7 @@ func TestGetOrSet(t *testing.T) { } func TestSetIfLessThanLimit(t *testing.T) { - s := store.Store[int]{} + s := store.Store[string, int]{} limit := 2 @@ -316,7 +316,7 @@ func TestSetIfLessThanLimit(t *testing.T) { } func TestUnmarshalJSON(t *testing.T) { - s := store.Store[string]{} + s := store.Store[string, string]{} s.Set("b", "old") // should be overwritten s.Set("c", "test3") // ensures that the old values are not removed @@ -339,7 +339,7 @@ func TestUnmarshalJSON(t *testing.T) { } func TestMarshalJSON(t *testing.T) { - s := &store.Store[string]{} + s := &store.Store[string, string]{} s.Set("a", "test1") s.Set("b", "test2") @@ -356,7 +356,7 @@ func TestMarshalJSON(t *testing.T) { } func TestShrink(t *testing.T) { - s := &store.Store[int]{} + s := &store.Store[string, int]{} total := 1000 diff --git a/tools/subscriptions/broker.go b/tools/subscriptions/broker.go index f2675eb4..82cf30d1 100644 --- a/tools/subscriptions/broker.go +++ b/tools/subscriptions/broker.go @@ -9,13 +9,13 @@ import ( // Broker defines a struct for managing subscriptions clients. type Broker struct { - store *store.Store[Client] + store *store.Store[string, Client] } // NewBroker initializes and returns a new Broker instance. func NewBroker() *Broker { return &Broker{ - store: store.New[Client](nil), + store: store.New[string, Client](nil), } } diff --git a/tools/template/registry.go b/tools/template/registry.go index 57af4b29..24402069 100644 --- a/tools/template/registry.go +++ b/tools/template/registry.go @@ -37,7 +37,7 @@ import ( // Use the Registry.Load* methods to load templates into the registry. func NewRegistry() *Registry { return &Registry{ - cache: store.New[*Renderer](nil), + cache: store.New[string, *Renderer](nil), funcs: template.FuncMap{ "raw": func(str string) template.HTML { return template.HTML(str) @@ -50,7 +50,7 @@ func NewRegistry() *Registry { // // Use the Registry.Load* methods to load templates into the registry. type Registry struct { - cache *store.Store[*Renderer] + cache *store.Store[string, *Renderer] funcs template.FuncMap }