diff --git a/grade/internal/infrastructure/seedwork/identity/map.go b/grade/internal/infrastructure/seedwork/identity/map.go new file mode 100644 index 00000000..16f84b0a --- /dev/null +++ b/grade/internal/infrastructure/seedwork/identity/map.go @@ -0,0 +1,63 @@ +package identity + +import ( + "errors" + + "github.com/emacsway/grade/grade/pkg/collections" +) + +var ( + ErrObjectAlreadyWatched = errors.New("") + ErrObjectNotFound = errors.New("") +) + +type IdentityMap[K comparable, V any] struct { + manageable collections.ReplacingMap[K, V] + isolation IsolationStrategy[K, V] +} + +func NewIdentityMap[K comparable, V any](size uint) *IdentityMap[K, V] { + manageable := collections.NewReplacingMap[K, V](size) + isolation := serializableStrategy[K, V]{manageable: manageable} + + return &IdentityMap[K, V]{ + manageable: manageable, + isolation: &isolation, + } +} + +func (im *IdentityMap[K, V]) Add(key K, object V) (bool, error) { + if err := im.isolation.add(key, object); err != nil { + return false, err + } + + return true, nil +} + +func (im *IdentityMap[K, V]) Get(key K) (object V, err error) { + return im.isolation.get(key) +} + +func (im *IdentityMap[K, V]) Has(key K) bool { + return im.isolation.has(key) +} + +func (im *IdentityMap[K, V]) SetSize(size uint) { + im.manageable.SetSize(size) +} + +func (im *IdentityMap[K, V]) SetIsolationLevel(level IsolationLevel) { + + switch level { + case ReadUncommitted: + im.isolation = &readUncommittedStrategy[K, V]{manageable: im.manageable} + case RepeatableReads: + im.isolation = &repeatableReadsStrategy[K, V]{manageable: im.manageable} + case Serializable: + im.isolation = &serializableStrategy[K, V]{manageable: im.manageable} + case ReadCommitted: + im.isolation = &readCommittedStrategy[K, V]{manageable: im.manageable} + default: + im.isolation = &serializableStrategy[K, V]{manageable: im.manageable} + } +} diff --git a/grade/internal/infrastructure/seedwork/identity/strategy.go b/grade/internal/infrastructure/seedwork/identity/strategy.go new file mode 100644 index 00000000..2ad032ae --- /dev/null +++ b/grade/internal/infrastructure/seedwork/identity/strategy.go @@ -0,0 +1,91 @@ +package identity + +import ( + "errors" + + "github.com/emacsway/grade/grade/pkg/collections" +) + +type IsolationLevel uint + +const ( + ReadUncommitted IsolationLevel = iota + ReadCommitted = iota + RepeatableReads = iota + Serializable = iota +) + +var ( + ErrNonexistentObject = errors.New("") + ErrDeniedOperationForStrategy = errors.New("") +) + +type IsolationStrategy[K comparable, V any] interface { + add(key K, object V) error + get(key K) (V, error) + has(key K) bool +} + +type readUncommittedStrategy[K comparable, V any] struct { + manageable collections.ReplacingMap[K, V] +} + +func (r *readUncommittedStrategy[K, V]) add(key K, object V) error { + return nil +} + +func (r *readUncommittedStrategy[K, V]) get(key K) (object V, err error) { + return object, ErrDeniedOperationForStrategy +} + +func (r *readUncommittedStrategy[K, V]) has(key K) bool { + return false +} + +type readCommittedStrategy[K comparable, V any] struct { + manageable collections.ReplacingMap[K, V] +} + +func (r *readCommittedStrategy[K, V]) add(key K, object V) error { + return nil +} + +func (r *readCommittedStrategy[K, V]) get(key K) (object V, err error) { + return object, nil +} + +func (r *readCommittedStrategy[K, V]) has(key K) bool { + return false +} + +type repeatableReadsStrategy[K comparable, V any] struct { + manageable collections.ReplacingMap[K, V] +} + +func (r *repeatableReadsStrategy[K, V]) add(key K, object V) error { + return nil +} + +func (r *repeatableReadsStrategy[K, V]) get(key K) (V, error) { + return r.manageable.Get(key) +} + +func (r *repeatableReadsStrategy[K, V]) has(key K) bool { + return r.manageable.Has(key) +} + +type serializableStrategy[K comparable, V any] struct { + manageable collections.ReplacingMap[K, V] +} + +func (s *serializableStrategy[K, V]) add(key K, object V) error { + return nil +} + +func (s *serializableStrategy[K, V]) get(key K) (V, error) { + return s.manageable.Get(key) +} + +func (s *serializableStrategy[K, V]) has(key K) bool { + return s.manageable.Has(key) +} diff --git a/grade/pkg/collections/map.go b/grade/pkg/collections/map.go new file mode 100644 index 00000000..ad832391 --- /dev/null +++ b/grade/pkg/collections/map.go @@ -0,0 +1,103 @@ +package collections + +import ( + "container/list" + "errors" +) + +var ( + ErrKeyDoesNotContains = errors.New("") +) + +type CachedMap[K comparable, V any] struct { + m map[K]V +} + +func NewCachedMap[K comparable, V any]() CachedMap[K, V] { + return CachedMap[K, V]{ + m: map[K]V{}, + } +} + +func (m CachedMap[K, V]) Add(key K, value V) { + m.m[key] = value +} + +func (m CachedMap[K, V]) Get(key K) (value V, err error) { + if value, found := m.m[key]; found { + return value, nil + } + + return value, ErrKeyDoesNotContains +} + +func (m CachedMap[K, V]) Remove(key K) { + delete(m.m, key) +} + +func (m CachedMap[K, V]) Has(key K) bool { + _, found := m.m[key] + return found +} + +type item[K comparable, V any] struct { + key K + value V +} + +type ReplacingMap[K comparable, V any] struct { + items map[K]*list.Element + order *list.List + size uint +} + +func NewReplacingMap[K comparable, V any](size uint) ReplacingMap[K, V] { + return ReplacingMap[K, V]{ + items: make(map[K]*list.Element, size), + order: list.New(), + size: size, + } +} + +func (m *ReplacingMap[K, V]) Add(key K, value V) { + element := m.order.PushBack(item[K, V]{key, value}) + m.items[key] = element + + if (uint)(len(m.items)) > m.size { + element = m.order.Front() + m.order.Remove(element) + delete(m.items, element.Value.(item[K, V]).key) + } +} + +func (m *ReplacingMap[K, V]) Get(key K) (value V, err error) { + if element, found := m.items[key]; found { + return element.Value.(item[K, V]).value, nil + } + + return value, ErrKeyDoesNotContains +} + +func (m *ReplacingMap[K, V]) Touch(key K) { + element := m.items[key] + m.order.MoveToBack(element) +} + +func (m *ReplacingMap[K, V]) Remove(key K) { + element, found := m.items[key] + if !found { + return + } + + delete(m.items, key) + m.order.Remove(element) +} + +func (m *ReplacingMap[K, V]) Has(key K) bool { + _, found := m.items[key] + return found +} + +func (m *ReplacingMap[K, V]) SetSize(size uint) { + m.size = size +} diff --git a/grade/pkg/collections/map_test.go b/grade/pkg/collections/map_test.go new file mode 100644 index 00000000..85f3f590 --- /dev/null +++ b/grade/pkg/collections/map_test.go @@ -0,0 +1,173 @@ +package collections + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCachedMap(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + assertion func(t *testing.T, cm CachedMap[string, string]) + }{ + { + name: "must contains a set key", + assertion: func(t *testing.T, cm CachedMap[string, string]) { + cm.Add("1", "1") + assert.Equal(t, true, cm.Has("1")) + }, + }, + + { + name: "must not contain a removed key", + assertion: func(t *testing.T, cm CachedMap[string, string]) { + cm.Add("1", "1") + cm.Remove("1") + + assert.Equal(t, false, cm.Has("1")) + }, + }, + + { + name: "the key can be reused", + assertion: func(t *testing.T, cm CachedMap[string, string]) { + cm.Add("1", "1") + cm.Remove("1") + cm.Add("1", "2") + + val, _ := cm.Get("1") + assert.Equal(t, "2", val) + }, + }, + + { + name: "trying to access an unset key", + assertion: func(t *testing.T, cm CachedMap[string, string]) { + _, err := cm.Get("1") + assert.Equal(t, ErrKeyDoesNotContains, err) + }, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + cm := NewCachedMap[string, string]() + tt.assertion(t, cm) + }) + } +} + +func TestReplacingMapMap(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + assertion func(t *testing.T, cm ReplacingMap[string, string]) + }{ + { + name: "must contains a set key", + assertion: func(t *testing.T, cm ReplacingMap[string, string]) { + cm.Add("1", "1") + assert.Equal(t, true, cm.Has("1")) + }, + }, + + { + name: "must not contain a removed key", + assertion: func(t *testing.T, cm ReplacingMap[string, string]) { + cm.Add("1", "1") + cm.Remove("1") + + assert.Equal(t, false, cm.Has("1")) + }, + }, + + { + name: "the key can be reused", + assertion: func(t *testing.T, cm ReplacingMap[string, string]) { + cm.Add("1", "1") + cm.Remove("1") + cm.Add("1", "2") + + val, _ := cm.Get("1") + assert.Equal(t, "2", val) + }, + }, + + { + name: "trying to access an unset key", + assertion: func(t *testing.T, cm ReplacingMap[string, string]) { + _, err := cm.Get("1") + assert.Equal(t, ErrKeyDoesNotContains, err) + }, + }, + + { + name: "trying to remove an unset key", + assertion: func(t *testing.T, cm ReplacingMap[string, string]) { + cm.Remove("1") + cm.Add("1", "1") + + val, _ := cm.Get("1") + assert.Equal(t, "1", val) + assert.Equal(t, true, cm.Has("1")) + }, + }, + + { + name: "touched key must be in map", + assertion: func(t *testing.T, cm ReplacingMap[string, string]) { + + cm.SetSize(2) + + cm.Add("1", "1") + cm.Add("2", "2") + + cm.Touch("1") + cm.Add("3", "3") + + assert.Equal(t, true, cm.Has("1")) + assert.Equal(t, true, cm.Has("3")) + + assert.Equal(t, false, cm.Has("2")) + }, + }, + + { + name: "trying to access to replaced key", + assertion: func(t *testing.T, cm ReplacingMap[string, string]) { + + cm.SetSize(2) + + cm.Add("1", "1") + cm.Add("2", "2") + cm.Add("3", "3") + + _, err := cm.Get("1") + assert.Equal(t, ErrKeyDoesNotContains, err) + assert.Equal(t, false, cm.Has("1")) + + assert.Equal(t, true, cm.Has("2")) + assert.Equal(t, true, cm.Has("3")) + + val, _ := cm.Get("2") + assert.Equal(t, "2", val) + + val, _ = cm.Get("3") + assert.Equal(t, "3", val) + }, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + cm := NewReplacingMap[string, string](3) + tt.assertion(t, cm) + }) + } +}