~whereswaldon/forest-go

3d630678535f314acf9f969895ad88fcc7980948 — Chris Waldon 2 years ago e01bbe4 + c236655
Merge remote-tracking branch 'github/expand-store'
2 files changed, 195 insertions(+), 31 deletions(-)

M store.go
M store_test.go
M store.go => store.go +110 -16
@@ 1,6 1,8 @@
package forest

import (
	"fmt"

	"git.sr.ht/~whereswaldon/forest-go/fields"
)



@@ 8,15 10,26 @@ type Store interface {
	Size() (int, error)
	CopyInto(Store) error
	Get(*fields.QualifiedHash) (Node, bool, error)
	GetIdentity(*fields.QualifiedHash) (Node, bool, error)
	GetCommunity(*fields.QualifiedHash) (Node, bool, error)
	GetConversation(communityID, conversationID *fields.QualifiedHash) (Node, bool, error)
	GetReply(communityID, conversationID, replyID *fields.QualifiedHash) (Node, bool, error)
	Children(*fields.QualifiedHash) ([]*fields.QualifiedHash, error)
	Add(Node) error
}

type MemoryStore struct {
	Items map[string]Node
	Items    map[string]Node
	ChildMap map[string][]string
}

var _ Store = &MemoryStore{}

func NewMemoryStore() *MemoryStore {
	return &MemoryStore{make(map[string]Node)}
	return &MemoryStore{
		Items:    make(map[string]Node),
		ChildMap: make(map[string][]string),
	}
}

func (m *MemoryStore) Size() (int, error) {


@@ 40,11 53,46 @@ func (m *MemoryStore) Get(id *fields.QualifiedHash) (Node, bool, error) {
	return m.GetID(idString)
}

func (m *MemoryStore) GetIdentity(id *fields.QualifiedHash) (Node, bool, error) {
	return m.Get(id)
}

func (m *MemoryStore) GetCommunity(id *fields.QualifiedHash) (Node, bool, error) {
	return m.Get(id)
}

func (m *MemoryStore) GetConversation(communityID, conversationID *fields.QualifiedHash) (Node, bool, error) {
	return m.Get(conversationID)
}

func (m *MemoryStore) GetReply(communityID, conversationID, replyID *fields.QualifiedHash) (Node, bool, error) {
	return m.Get(replyID)
}

func (m *MemoryStore) GetID(id string) (Node, bool, error) {
	item, has := m.Items[id]
	return item, has, nil
}

func (m *MemoryStore) Children(id *fields.QualifiedHash) ([]*fields.QualifiedHash, error) {
	idString, err := id.MarshalString()
	if err != nil {
		return nil, fmt.Errorf("failed to marshal node id into key: %w", err)
	}
	children, any := m.ChildMap[idString]
	if !any {
		return []*fields.QualifiedHash{}, nil
	}
	childIDs := make([]*fields.QualifiedHash, len(children))
	for i, childStr := range children {
		childIDs[i] = &fields.QualifiedHash{}
		if err := childIDs[i].UnmarshalText([]byte(childStr)); err != nil {
			return nil, fmt.Errorf("failed to transform key back into node id: %w", err)
		}
	}
	return childIDs, nil
}

func (m *MemoryStore) Add(node Node) error {
	id, err := node.ID().MarshalString()
	if err != nil {


@@ 59,6 107,11 @@ func (m *MemoryStore) AddID(id string, node Node) error {
		return nil
	}
	m.Items[id] = node
	parentID, err := node.ParentID().MarshalString()
	if err != nil {
		return fmt.Errorf("failed to marshal string of parent node: %w", err)
	}
	m.ChildMap[parentID] = append(m.ChildMap[parentID], id)
	return nil
}



@@ 71,6 124,8 @@ type CacheStore struct {
	Cache, Back Store
}

var _ Store = &CacheStore{}

// NewCacheStore creates a single logical store from the given two stores.
// All items from `cache` are automatically copied into `base` during
// the construction of the CacheStore, and from then on (assuming


@@ 95,20 150,7 @@ func (m *CacheStore) Size() (int, error) {
// If the cache is missed by the backing store is hit, the node will automatically be
// added to the cache.
func (m *CacheStore) Get(id *fields.QualifiedHash) (Node, bool, error) {
	if node, has, err := m.Cache.Get(id); err != nil {
		return nil, false, err
	} else if has {
		return node, has, nil
	}
	if node, has, err := m.Back.Get(id); err != nil {
		return nil, false, err
	} else if has {
		if err := m.Cache.Add(node); err != nil {
			return nil, false, err
		}
		return node, has, nil
	}
	return nil, false, nil
	return m.getUsingFuncs(id, m.Cache.Get, m.Back.Get)
}

func (m *CacheStore) CopyInto(other Store) error {


@@ 125,3 167,55 @@ func (m *CacheStore) Add(node Node) error {
	}
	return nil
}

func (m *CacheStore) getUsingFuncs(id *fields.QualifiedHash, getter1, getter2 func(*fields.QualifiedHash) (Node, bool, error)) (Node, bool, error) {
	cacheNode, inCache, err := getter1(id)
	if err != nil {
		return nil, false, fmt.Errorf("failed fetching id from cache: %w", err)
	}
	if inCache {
		return cacheNode, inCache, err
	}
	backNode, inBackingStore, err := getter2(id)
	if err != nil {
		return nil, false, fmt.Errorf("failed fetching id from cache: %w", err)
	}
	if inBackingStore {
		if err := m.Cache.Add(backNode); err != nil {
			return nil, false, fmt.Errorf("failed to up-propagate node into cache: %w", err)
		}
	}
	return backNode, inBackingStore, err
}

func (m *CacheStore) GetIdentity(id *fields.QualifiedHash) (Node, bool, error) {
	return m.getUsingFuncs(id, m.Cache.GetIdentity, m.Back.GetIdentity)
}

func (m *CacheStore) GetCommunity(id *fields.QualifiedHash) (Node, bool, error) {
	return m.getUsingFuncs(id, m.Cache.GetCommunity, m.Back.GetCommunity)
}

func (m *CacheStore) GetConversation(communityID, conversationID *fields.QualifiedHash) (Node, bool, error) {
	return m.getUsingFuncs(communityID, // this id is irrelevant
		func(*fields.QualifiedHash) (Node, bool, error) {
			return m.Cache.GetConversation(communityID, conversationID)
		},
		func(*fields.QualifiedHash) (Node, bool, error) {
			return m.Back.GetConversation(communityID, conversationID)
		})
}

func (m *CacheStore) GetReply(communityID, conversationID, replyID *fields.QualifiedHash) (Node, bool, error) {
	return m.getUsingFuncs(communityID, // this id is irrelevant
		func(*fields.QualifiedHash) (Node, bool, error) {
			return m.Cache.GetReply(communityID, conversationID, replyID)
		},
		func(*fields.QualifiedHash) (Node, bool, error) {
			return m.Back.GetReply(communityID, conversationID, replyID)
		})
}

func (m *CacheStore) Children(id *fields.QualifiedHash) ([]*fields.QualifiedHash, error) {
	return m.Back.Children(id)
}

M store_test.go => store_test.go +85 -15
@@ 3,7 3,8 @@ package forest_test
import (
	"testing"

	"git.sr.ht/~whereswaldon/forest-go"
	forest "git.sr.ht/~whereswaldon/forest-go"
	"git.sr.ht/~whereswaldon/forest-go/fields"
)

func TestMemoryStore(t *testing.T) {


@@ 17,17 18,37 @@ func testStandardStoreInterface(t *testing.T, s forest.Store, storeImplName stri
	} else if err != nil {
		t.Errorf("Expected new %s Size() to succeed, failed with %s", storeImplName, err)
	}
	id, _, com, rep := MakeReplyOrSkip(t)
	nodes := []forest.Node{id, com, rep}
	// create three test nodes, one of each type
	identity, _, community, reply := MakeReplyOrSkip(t)
	nodes := []forest.Node{identity, community, reply}

	// create a set of functions that perform different "Get" operations on nodes
	getFuncs := map[string]func(*fields.QualifiedHash) (forest.Node, bool, error){
		"get":       s.Get,
		"identity":  s.GetIdentity,
		"community": s.GetCommunity,
		"conversation": func(id *fields.QualifiedHash) (forest.Node, bool, error) {
			return s.GetConversation(community.ID(), id)
		},
		"reply": func(id *fields.QualifiedHash) (forest.Node, bool, error) {
			return s.GetReply(community.ID(), reply.ID(), id)
		},
	}

	// ensure no getter functions succeed on an empty store
	for _, i := range nodes {
		if node, has, err := s.Get(i.ID()); has {
			t.Errorf("Empty %s should not contain element %v", storeImplName, i.ID())
		} else if err != nil {
			t.Errorf("Empty %s Get() should not err with %s", storeImplName, err)
		} else if node != nil {
			t.Errorf("Empty %s Get() should return none-nil node %v", storeImplName, node)
		for _, get := range getFuncs {
			if node, has, err := get(i.ID()); has {
				t.Errorf("Empty %s should not contain element %v", storeImplName, i.ID())
			} else if err != nil {
				t.Errorf("Empty %s Get() should not err with %s", storeImplName, err)
			} else if node != nil {
				t.Errorf("Empty %s Get() should return none-nil node %v", storeImplName, node)
			}
		}
	}

	// add each node
	for count, i := range nodes {
		if err := s.Add(i); err != nil {
			t.Errorf("%s Add() should not err on Add(): %s", storeImplName, err)


@@ 37,14 58,63 @@ func testStandardStoreInterface(t *testing.T, s forest.Store, storeImplName stri
		} else if size != count+1 {
			t.Errorf("%s Size() should be %d after %d Add()s, got %d", storeImplName, count+1, count+1, size)
		}
		if node, has, err := s.Get(i.ID()); !has {
			t.Errorf("%s should contain element %v", storeImplName, i.ID())
		} else if err != nil {
			t.Errorf("%s Has() should not err with %s", storeImplName, err)
		} else if !i.Equals(node) {
			t.Errorf("%s Get() should return a node equal to the one that was Add()ed. Got %v, expected %v", storeImplName, node, i)
	}

	// map each node to the getters that should be successful in fetching it
	nodesToGetters := []struct {
		forest.Node
		funcs []string
	}{
		{identity, []string{"get", "identity"}},
		{community, []string{"get", "community"}},
		{reply, []string{"get", "conversation", "reply"}},
	}

	// ensure all getters work for each node
	for _, getterConfig := range nodesToGetters {
		currentNode := getterConfig.Node
		for _, getterName := range getterConfig.funcs {
			if node, has, err := getFuncs[getterName](currentNode.ID()); !has {
				t.Errorf("%s should contain element %v", storeImplName, currentNode.ID())
			} else if err != nil {
				t.Errorf("%s Has() should not err with %s", storeImplName, err)
			} else if !currentNode.Equals(node) {
				t.Errorf("%s Get() should return a node equal to the one that was Add()ed. Got %v, expected %v", storeImplName, node, currentNode)
			}
		}
	}

    // map nodes to the children that they ought to have within the store
	nodesToChildren := []struct {
		forest.Node
		children []*fields.QualifiedHash
	}{
		{identity, []*fields.QualifiedHash{}},
		{community, []*fields.QualifiedHash{reply.ID()}},
		{reply, []*fields.QualifiedHash{}},
	}

    // check each node has its proper children
	for _, childConfig := range nodesToChildren {
		if children, err := s.Children(childConfig.ID()); err != nil {
			t.Errorf("%s should not error fetching children of %v", storeImplName, childConfig.ID())
		} else {
			for _, child := range childConfig.children {
				if !containsID(children, child) {
					t.Errorf("%s should have %v as a child of %v", storeImplName, child, childConfig.ID())
				}
			}
		}
	}
}

func containsID(ids []*fields.QualifiedHash, id *fields.QualifiedHash) bool {
	for _, current := range ids {
		if current.Equals(id) {
			return true
		}
	}
	return false
}

func TestCacheStore(t *testing.T) {