#89 Generalise the three Storage interfaces and structs into one generic storage manager.

This commit is contained in:
Maximilian Paß
2022-06-21 19:06:55 +02:00
parent 542be96c66
commit 34040162c2
22 changed files with 292 additions and 554 deletions

107
pkg/storage/storage.go Normal file
View File

@ -0,0 +1,107 @@
package storage
import (
"sync"
)
// Storage is an interface for storing objects.
type Storage[T any] interface {
// List returns all objects from the storage.
List() []T
// Add adds an object to the storage.
// It overwrites the old object if one with the same id was already stored.
Add(id string, o T)
// Get returns an object from the storage.
// Iff the object does not exist in the storage, ok will be false.
Get(id string) (o T, ok bool)
// Delete deletes the object with the passed id from the storage.
// It does nothing if no object with the id is present in the store.
Delete(id string)
// Pop deletes the object with the given id from the storage and returns it.
// Iff no such execution exists, ok is false and true otherwise.
Pop(id string) (o T, ok bool)
// Purge removes all objects from the storage.
Purge()
// Length returns the number of currently stored objects in the storage.
Length() uint
// Sample returns and removes an arbitrary object from the storage.
// ok is true iff an object was returned.
Sample() (o T, ok bool)
}
// localStorage stores objects in the local application memory.
type localStorage[T any] struct {
sync.RWMutex
objects map[string]T
}
// NewLocalStorage responds with a Storage implementation.
// This implementation stores the data thread-safe in the local application memory.
func NewLocalStorage[T any]() *localStorage[T] {
return &localStorage[T]{
objects: make(map[string]T),
}
}
func (s *localStorage[T]) List() (o []T) {
s.RLock()
defer s.RUnlock()
for _, value := range s.objects {
o = append(o, value)
}
return o
}
func (s *localStorage[T]) Add(id string, o T) {
s.Lock()
defer s.Unlock()
s.objects[id] = o
}
func (s *localStorage[T]) Get(id string) (o T, ok bool) {
s.RLock()
defer s.RUnlock()
o, ok = s.objects[id]
return
}
func (s *localStorage[T]) Delete(id string) {
s.Lock()
defer s.Unlock()
delete(s.objects, id)
}
func (s *localStorage[T]) Pop(id string) (T, bool) {
o, ok := s.Get(id)
s.Delete(id)
return o, ok
}
func (s *localStorage[T]) Purge() {
s.Lock()
defer s.Unlock()
s.objects = make(map[string]T)
}
func (s *localStorage[T]) Sample() (o T, ok bool) {
s.Lock()
defer s.Unlock()
for key, object := range s.objects {
delete(s.objects, key)
return object, true
}
return
}
func (s *localStorage[T]) Length() uint {
s.RLock()
defer s.RUnlock()
return uint(len(s.objects))
}

100
pkg/storage/storage_test.go Normal file
View File

@ -0,0 +1,100 @@
package storage
import (
"github.com/stretchr/testify/suite"
"testing"
)
func TestRunnerPoolTestSuite(t *testing.T) {
suite.Run(t, new(ObjectPoolTestSuite))
}
type ObjectPoolTestSuite struct {
suite.Suite
objectStorage *localStorage[any]
object int
}
func (s *ObjectPoolTestSuite) SetupTest() {
s.objectStorage = NewLocalStorage[any]()
s.object = 42
}
func (s *ObjectPoolTestSuite) TestAddedObjectCanBeRetrieved() {
s.objectStorage.Add("my_id", s.object)
retrievedRunner, ok := s.objectStorage.Get("my_id")
s.True(ok, "A saved object should be retrievable")
s.Equal(s.object, retrievedRunner)
}
func (s *ObjectPoolTestSuite) TestObjectWithSameIdOverwritesOldOne() {
otherObject := 21
// assure object is actually different
s.NotEqual(s.object, otherObject)
s.objectStorage.Add("my_id", s.object)
s.objectStorage.Add("my_id", otherObject)
retrievedObject, _ := s.objectStorage.Get("my_id")
s.NotEqual(s.object, retrievedObject)
s.Equal(otherObject, retrievedObject)
}
func (s *ObjectPoolTestSuite) TestDeletedObjectsAreNotAccessible() {
s.objectStorage.Add("my_id", s.object)
s.objectStorage.Delete("my_id")
retrievedObject, ok := s.objectStorage.Get("my_id")
s.Nil(retrievedObject)
s.False(ok, "A deleted object should not be accessible")
}
func (s *ObjectPoolTestSuite) TestSampleReturnsObjectWhenOneIsAvailable() {
s.objectStorage.Add("my_id", s.object)
sampledObject, ok := s.objectStorage.Sample()
s.NotNil(sampledObject)
s.True(ok)
}
func (s *ObjectPoolTestSuite) TestSampleReturnsFalseWhenNoneIsAvailable() {
sampledObject, ok := s.objectStorage.Sample()
s.Nil(sampledObject)
s.False(ok)
}
func (s *ObjectPoolTestSuite) TestSampleRemovesObjectFromPool() {
s.objectStorage.Add("my_id", s.object)
_, _ = s.objectStorage.Sample()
_, ok := s.objectStorage.Get("my_id")
s.False(ok)
}
func (s *ObjectPoolTestSuite) TestLenOfEmptyPoolIsZero() {
s.Equal(uint(0), s.objectStorage.Length())
}
func (s *ObjectPoolTestSuite) TestLenChangesOnStoreContentChange() {
s.Run("len increases when object is added", func() {
s.objectStorage.Add("my_id_1", s.object)
s.Equal(uint(1), s.objectStorage.Length())
})
s.Run("len does not increase when object with same id is added", func() {
s.objectStorage.Add("my_id_1", s.object)
s.Equal(uint(1), s.objectStorage.Length())
})
s.Run("len increases again when different object is added", func() {
anotherObject := 21
s.objectStorage.Add("my_id_2", anotherObject)
s.Equal(uint(2), s.objectStorage.Length())
})
s.Run("len decreases when object is deleted", func() {
s.objectStorage.Delete("my_id_1")
s.Equal(uint(1), s.objectStorage.Length())
})
s.Run("len decreases when object is sampled", func() {
_, _ = s.objectStorage.Sample()
s.Equal(uint(0), s.objectStorage.Length())
})
}