From 04cd77b6475377d13f4ee5b901c1e37fd215b99e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Janvier?= <57535343+timtimjnvr@users.noreply.github.com> Date: Sat, 11 Nov 2023 17:18:59 +0100 Subject: [PATCH] feat: use generic to make linked list usable for both chats and nodes (#70) * savings * fix(storage): tests * changes: finiesh fixing all tests * feat: remove slot from storage if it is not used any more --- crdt/chat.go | 100 +++++++++++++++++------------------ crdt/chat_test.go | 76 ++++---------------------- orchestrator/orchestrator.go | 12 ++--- storage/linked.go | 29 +++++----- storage/linked_test.go | 1 + storage/storage.go | 100 +++++++++++++++++++++++++---------- storage/storage_test.go | 58 ++++++++++---------- 7 files changed, 177 insertions(+), 199 deletions(-) diff --git a/crdt/chat.go b/crdt/chat.go index 79c0932..33c3526 100644 --- a/crdt/chat.go +++ b/crdt/chat.go @@ -4,7 +4,6 @@ import ( "encoding/json" "github.com/google/uuid" "github.com/pkg/errors" - "log" "time" ) @@ -12,7 +11,7 @@ type ( Chat struct { Id uuid.UUID `json:"id"` Name string `json:"name"` - nodesInfos []*NodeInfos + nodesSlots []uint8 messages []*Message // ordered by date : 0 being the oldest message, 1 coming after 0 etc ... } ) @@ -25,7 +24,7 @@ func NewChat(name string) *Chat { return &Chat{ Id: uuid.New(), Name: name, - nodesInfos: make([]*NodeInfos, 0, maxNumberOfNodes), + nodesSlots: make([]uint8, 0, maxNumberOfNodes), messages: make([]*Message, 0, maxNumberOfMessages), } } @@ -38,70 +37,67 @@ func (c *Chat) GetName() string { return c.Name } -func (c *Chat) SaveNode(nodeInfo *NodeInfos) { - // update if found - for i, n := range c.nodesInfos { - if n.Id == nodeInfo.Id { - c.nodesInfos[i] = nodeInfo +func (c *Chat) SaveNode(nodeSlot uint8) { + + // check if present + for _, s := range c.nodesSlots { + if s == nodeSlot { return } } // append if not found - c.nodesInfos = append(c.nodesInfos, nodeInfo) + c.nodesSlots = append(c.nodesSlots, nodeSlot) } -func (c *Chat) RemoveNodeBySlot(slot uint8) (string, error) { +func (c *Chat) RemoveNodeSlot(slot uint8) error { // get index var ( index int found bool - n *NodeInfos + s uint8 ) - for index, n = range c.nodesInfos { - if n.Slot == slot { + + for index, s = range c.nodesSlots { + if s == slot { found = true break } } if !found { - return "", NotFoundErr + return NotFoundErr } - nodeName := c.nodesInfos[index].Name - - if index == 0 && len(c.nodesInfos) == 1 { - c.nodesInfos = make([]*NodeInfos, 0, 0) - return nodeName, nil + if index == 0 && len(c.nodesSlots) == 1 { + c.nodesSlots = make([]uint8, 0, 0) + return nil } - if index == 0 && len(c.nodesInfos) > 1 { - c.nodesInfos = c.nodesInfos[index+1:] - return nodeName, nil + if index == 0 && len(c.nodesSlots) > 1 { + c.nodesSlots = c.nodesSlots[index+1:] + return nil } - if index == len(c.nodesInfos)-1 { - c.nodesInfos = c.nodesInfos[:len(c.nodesInfos)-1] - return nodeName, nil - + if index == len(c.nodesSlots)-1 { + c.nodesSlots = c.nodesSlots[:len(c.nodesSlots)-1] + return nil } var ( - newNodeInfos = make([]*NodeInfos, len(c.nodesInfos)-1) - j int + newNodesSlots = make([]uint8, len(c.nodesSlots)-1) + j int ) - for i := 0; i <= len(c.nodesInfos)-1; i++ { + for i := 0; i <= len(c.nodesSlots)-1; i++ { if i == index { continue } - newNodeInfos[j] = c.nodesInfos[i] + newNodesSlots[j] = c.nodesSlots[i] j++ } - c.nodesInfos = newNodeInfos - return nodeName, nil - + c.nodesSlots = newNodesSlots + return nil } func (c *Chat) SaveMessage(message *Message) { @@ -154,40 +150,42 @@ func (c *Chat) ToBytes() []byte { // GetSlots returns all the slots identifying active TCP connections between nodes. func (c *Chat) GetSlots() []uint8 { length := 0 - if len(c.nodesInfos) > 0 { - length = len(c.nodesInfos) - 1 + if len(c.nodesSlots) > 0 { + length = len(c.nodesSlots) - 1 } slots := make([]uint8, 0, length) - for _, i := range c.nodesInfos { + for _, s := range c.nodesSlots { // My own slot - if i.Slot == 0 { + if s == 0 { continue } - slots = append(slots, i.Slot) + slots = append(slots, s) } return slots } -func (c *Chat) GetNodeBySlot(slot uint8) (*NodeInfos, error) { - for _, i := range c.nodesInfos { - if i.Slot == slot { - return i, nil +/* + func (c *Chat) GetNodeBySlot(slot uint8) (*NodeInfos, error) { + for _, s := range c.nodesSlots { + if s == slot { + return s, nil + } } - } - - return &NodeInfos{}, NotFoundErr -} + return &NodeInfos{}, NotFoundErr + } +*/ +/* func (c *Chat) DisplayUsers() { log.Printf("chat name : %s\n", c.Name) - for _, n := range c.nodesInfos { + for _, n := range c.nodesSlots { log.Printf("- %s (Address: %s, Port: %s, Slot: %d)\n", n.Name, n.Address, n.Port, n.Slot) } } - +*/ func (c *Chat) ContainsMessage(message *Message) bool { for _, m := range c.messages { if m.Id == message.Id { @@ -197,8 +195,9 @@ func (c *Chat) ContainsMessage(message *Message) bool { return false } -func (c *Chat) containsNode(id uuid.UUID) bool { - for _, n := range c.nodesInfos { +/* +func (c *Chat) containsNode(s uuid.UUID) bool { + for _, n := range c.nodesSlots { if n.Id == id { return true } @@ -206,3 +205,4 @@ func (c *Chat) containsNode(id uuid.UUID) bool { return false } +*/ diff --git a/crdt/chat_test.go b/crdt/chat_test.go index 372b0c0..af0d7f2 100644 --- a/crdt/chat_test.go +++ b/crdt/chat_test.go @@ -68,43 +68,17 @@ func TestChat_RemoveNodeBySlot(t *testing.T) { }{ { name: "delete first node", - slot: 0, + slot: 1, chat: &Chat{ - nodesInfos: []*NodeInfos{ - { - Id: idToDelete, - Slot: 0, - }, - { - Id: uuid.New(), - Slot: 1, - }, - { - Id: uuid.New(), - Slot: 2, - }, - }, + nodesSlots: []uint8{1, 2, 3}, }, expectedNumberOfNodes: 2, }, { name: "delete middle one", - slot: 1, + slot: 2, chat: &Chat{ - nodesInfos: []*NodeInfos{ - { - Id: uuid.New(), - Slot: 0, - }, - { - Id: idToDelete, - Slot: 1, - }, - { - Id: uuid.New(), - Slot: 2, - }, - }, + nodesSlots: []uint8{1, 2, 3}, }, expectedNumberOfNodes: 2, }, @@ -112,45 +86,15 @@ func TestChat_RemoveNodeBySlot(t *testing.T) { name: "delete middle one (4 elements)", slot: 2, chat: &Chat{ - nodesInfos: []*NodeInfos{ - { - Id: uuid.New(), - Slot: 0, - }, - { - Id: uuid.New(), - Slot: 1, - }, - { - Id: idToDelete, - Slot: 2, - }, - { - Id: uuid.New(), - Slot: 3, - }, - }, + nodesSlots: []uint8{1, 2, 3, 4}, }, expectedNumberOfNodes: 3, }, { name: "delete last", - slot: 2, + slot: 3, chat: &Chat{ - nodesInfos: []*NodeInfos{ - { - Id: uuid.New(), - Slot: 0, - }, - { - Id: uuid.New(), - Slot: 1, - }, - { - Id: idToDelete, - Slot: 2, - }, - }, + nodesSlots: []uint8{1, 2, 3}, }, expectedNumberOfNodes: 2, }, @@ -159,9 +103,9 @@ func TestChat_RemoveNodeBySlot(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - tt.chat.RemoveNodeBySlot(tt.slot) - assert.True(t, !tt.chat.containsNode(idToDelete)) - assert.Equal(t, tt.expectedNumberOfNodes, len(tt.chat.nodesInfos)) + tt.chat.RemoveNodeSlot(tt.slot) + assert.NotContains(t, idToDelete, tt.chat.nodesSlots) + assert.Equal(t, tt.expectedNumberOfNodes, len(tt.chat.nodesSlots)) }) } } diff --git a/orchestrator/orchestrator.go b/orchestrator/orchestrator.go index 63147da..0a529c4 100644 --- a/orchestrator/orchestrator.go +++ b/orchestrator/orchestrator.go @@ -112,12 +112,6 @@ func (o *Orchestrator) HandleChats(wg *sync.WaitGroup, toExecute chan *crdt.Oper continue } - // there is no chat specified in operation in this case we need to remove node identified by newNodeSlot from all chats - if op.Typology == crdt.RemoveNode { - o.storage.RemoveNodeSlotFromStorage(op.Slot) - continue - } - // for other operation we need to get a chat from storage chatID, err := uuid.Parse(op.TargetedChat) if err != nil { @@ -125,7 +119,7 @@ func (o *Orchestrator) HandleChats(wg *sync.WaitGroup, toExecute chan *crdt.Oper continue } - exists := o.storage.Exist(chatID) + exists := o.storage.ChatExist(chatID) if !exists { fmt.Printf(logErrFrmt, "unknown chat") continue @@ -154,7 +148,7 @@ func (o *Orchestrator) HandleChats(wg *sync.WaitGroup, toExecute chan *crdt.Oper // add other nodes slots, _ := o.storage.GetSlots(chatID) for _, s := range slots { - nodeInfo, err := o.storage.GetNodeFromChatBySlot(chatID, s) + nodeInfo, err := o.storage.GetNodeBySlot(s) if err != nil { fmt.Printf(logErrFrmt, err) } @@ -239,7 +233,7 @@ func (o *Orchestrator) HandleChats(wg *sync.WaitGroup, toExecute chan *crdt.Oper // Verify that slots are not used by any other chats for s, _ := range toDelete { - if o.storage.IsSlotUsedByOtherChats(s, o.myInfos.Id, chatID) { + if o.storage.IsSlotUsedByOtherChats(s, chatID) { toDelete[s] = false } } diff --git a/storage/linked.go b/storage/linked.go index 1a94b69..d0cb8ae 100644 --- a/storage/linked.go +++ b/storage/linked.go @@ -20,11 +20,10 @@ type ( next *element[T] } - list[T value] struct { + List[T value] struct { typeName string length int head *element[T] - tail *element[T] } ) @@ -36,14 +35,14 @@ var ( InvalidIdentifierErr = errors.New("invalid identifier") ) -func NewChatList() *list[*crdt.Chat] { - return &list[*crdt.Chat]{ +func NewChatList() *List[*crdt.Chat] { + return &List[*crdt.Chat]{ typeName: "chats", } } -func NewNodeList() *list[*crdt.NodeInfos] { - return &list[*crdt.NodeInfos]{ +func NewNodeList() *List[*crdt.NodeInfos] { + return &List[*crdt.NodeInfos]{ typeName: "nodes", } } @@ -55,11 +54,11 @@ func newElement[T value](v T) *element[T] { } } -func (l *list[T]) Len() int { +func (l *List[T]) Len() int { return l.length } -func (l *list[T]) Display() { +func (l *List[T]) Display() { fmt.Printf("%d %s\n", l.length, l.typeName) tmp := l.head @@ -71,7 +70,7 @@ func (l *list[T]) Display() { } // Add insert chat at the end of the listOld and return the key of the inserted chat -func (l *list[T]) Add(v T) (uuid.UUID, error) { +func (l *List[T]) Add(v T) (uuid.UUID, error) { e := newElement(v) id, err := uuid.Parse(v.GetID().String()) @@ -82,7 +81,6 @@ func (l *list[T]) Add(v T) (uuid.UUID, error) { if l.length == 0 { l.head = e - l.tail = e l.length++ return id, nil } @@ -103,7 +101,6 @@ func (l *list[T]) Add(v T) (uuid.UUID, error) { if ptr.next == nil { ptr.next = e - l.tail = e l.length++ } @@ -113,7 +110,7 @@ func (l *list[T]) Add(v T) (uuid.UUID, error) { return id, nil } -func (l *list[T]) Contains(id uuid.UUID) bool { +func (l *List[T]) Contains(id uuid.UUID) bool { if l.length == 0 { return false } @@ -130,7 +127,7 @@ func (l *list[T]) Contains(id uuid.UUID) bool { return false } -func (l *list[T]) Update(v T) error { +func (l *List[T]) Update(v T) error { if v == nil { return InvalidChatErr } @@ -157,7 +154,7 @@ func (l *list[T]) Update(v T) error { return NotFoundErr } -func (l *list[T]) GetByIndex(index int) (T, error) { +func (l *List[T]) GetByIndex(index int) (T, error) { if index >= l.Len() { return nil, NotFoundErr } @@ -173,7 +170,7 @@ func (l *list[T]) GetByIndex(index int) (T, error) { return tmp.v, nil } -func (l *list[T]) GetById(id uuid.UUID) (T, error) { +func (l *List[T]) GetById(id uuid.UUID) (T, error) { if l.length == 0 { return nil, NotFoundErr } @@ -190,7 +187,7 @@ func (l *list[T]) GetById(id uuid.UUID) (T, error) { return nil, NotFoundErr } -func (l *list[T]) Delete(id uuid.UUID) { +func (l *List[T]) Delete(id uuid.UUID) { if l.length == 0 { return } diff --git a/storage/linked_test.go b/storage/linked_test.go index 6a3b818..d5dd7f9 100644 --- a/storage/linked_test.go +++ b/storage/linked_test.go @@ -129,6 +129,7 @@ func TestList_Delete(t *testing.T) { l.Delete(second) ass.Equal(l.Len(), 0, "failed on Deleting remaining elementOld") + ass.Nil(l.head) first, _ = l.Add(crdt.NewChat("1")) second, _ = l.Add(crdt.NewChat("2")) diff --git a/storage/storage.go b/storage/storage.go index 3308150..873b3a8 100644 --- a/storage/storage.go +++ b/storage/storage.go @@ -5,12 +5,13 @@ import ( "github.com/google/uuid" "github.com/pkg/errors" "github/timtimjnvr/chat/crdt" + "log" ) type ( Storage struct { - chats *list[*crdt.Chat] - nodes *list[*crdt.NodeInfos] + chats *List[*crdt.Chat] + nodes *List[*crdt.NodeInfos] } ) @@ -21,13 +22,28 @@ func NewStorage() *Storage { } } -func (s *Storage) GetNodeFromChatBySlot(chatID uuid.UUID, slot uint8) (*crdt.NodeInfos, error) { - c, err := s.getChat(chatID.String(), true) - if err != nil { - return nil, err +func (s *Storage) GetNodeBySlot(slot uint8) (*crdt.NodeInfos, error) { + var ( + numberOfNodes = s.nodes.Len() + n *crdt.NodeInfos + err error + ) + if s.nodes.length == 0 { + return nil, NotFoundErr } - return c.GetNodeBySlot(slot) + for index := 0; index < numberOfNodes; index++ { + n, _ = s.nodes.GetByIndex(index) + if n.Slot == slot { + return n, nil + } + + if err != nil || n == nil { + return nil, NotFoundErr + } + } + + return n, nil } func (s *Storage) GetChatName(id uuid.UUID) (string, error) { @@ -39,7 +55,7 @@ func (s *Storage) GetChatName(id uuid.UUID) (string, error) { return c.Name, nil } -func (s *Storage) Exist(chatID uuid.UUID) bool { +func (s *Storage) ChatExist(chatID uuid.UUID) bool { return s.chats.Contains(chatID) } @@ -66,7 +82,6 @@ func (s *Storage) GetNewCurrentChatID() (uuid.UUID, error) { } func (s *Storage) AddMessageToChat(message *crdt.Message, chatID uuid.UUID) error { - c, err := s.getChat(chatID.String(), true) if err != nil { return err @@ -91,20 +106,37 @@ func (s *Storage) AddChat(chat *crdt.Chat) error { } func (s *Storage) RemoveChat(chatID uuid.UUID) { + slots, err := s.GetSlots(chatID) + if err != nil { + return + } + + for _, slot := range slots { + if !s.IsSlotUsedByOtherChats(slot, chatID) { + n, _ := s.GetNodeBySlot(slot) + s.nodes.Delete(n.Id) + } + } + s.chats.Delete(chatID) } -func (s *Storage) AddNodeToChat(nodeInfos *crdt.NodeInfos, chatID uuid.UUID) error { +// AddNodeToChat add a node to a given chat identified by id. The node slot need to be set +func (s *Storage) AddNodeToChat(node *crdt.NodeInfos, chatID uuid.UUID) error { + if !s.nodes.Contains(node.Id) { + _, _ = s.nodes.Add(node) + } + c, err := s.getChat(chatID.String(), false) if err != nil { return err } - c.SaveNode(nodeInfos) + c.SaveNode(node.Slot) return nil } -func (s *Storage) IsSlotUsedByOtherChats(slotToFind uint8, myNodeID uuid.UUID, exceptChatID uuid.UUID) bool { +func (s *Storage) IsSlotUsedByOtherChats(slotToFind uint8, excludeChatForSearch uuid.UUID) bool { var ( index = 0 numberOfChats = s.GetNumberOfChats() @@ -113,7 +145,7 @@ func (s *Storage) IsSlotUsedByOtherChats(slotToFind uint8, myNodeID uuid.UUID, e for index < numberOfChats && err == nil { tmpChat, _ := s.GetChatByIndex(index) - if tmpChat.Id == exceptChatID { + if tmpChat.Id == excludeChatForSearch { index++ continue } @@ -136,23 +168,23 @@ func (s *Storage) RemoveNodeFromChat(nodeSlot uint8, chatID uuid.UUID) error { return err } - _, err = c.RemoveNodeBySlot(nodeSlot) - return err -} + err = c.RemoveNodeSlot(nodeSlot) + n, err := s.GetNodeBySlot(nodeSlot) + if err != nil { + return err + } -func (s *Storage) GetChatByIndex(index int) (*crdt.Chat, error) { - return s.chats.GetByIndex(index) -} + fmt.Printf("%s leaved chat\n", n.Name) -func (s *Storage) SaveChat(c *crdt.Chat) error { - if !s.chats.Contains(c.Id) { - _, err := s.chats.Add(c) - if err != nil { - return err - } + if !s.IsSlotUsedByOtherChats(n.Slot, c.Id) { + s.nodes.Delete(n.Id) } - return s.chats.Update(c) + return nil +} + +func (s *Storage) GetChatByIndex(index int) (*crdt.Chat, error) { + return s.chats.GetByIndex(index) } func (s *Storage) DisplayChats() { @@ -165,7 +197,12 @@ func (s *Storage) DisplayChatUsers(chatID uuid.UUID) error { return err } - c.DisplayUsers() + log.Printf("chat name : %s\n", c.Name) + for slot := range c.GetSlots() { + n, _ := s.nodes.GetByIndex(slot) + log.Printf("- %s (Address: %s, Port: %s, Slot: %d)\n", n.Name, n.Address, n.Port, n.Slot) + } + return nil } @@ -181,6 +218,11 @@ func (s *Storage) RemoveNodeSlotFromStorage(slot uint8) { err error ) + node, err := s.GetNodeBySlot(slot) + if err != nil { + return + } + for index < numberOfChats && err == nil { c, err = s.GetChatByIndex(index) if err != nil { @@ -188,9 +230,9 @@ func (s *Storage) RemoveNodeSlotFromStorage(slot uint8) { continue } - nodeName, err2 := c.RemoveNodeBySlot(slot) + err2 := c.RemoveNodeSlot(slot) if err2 == nil { - fmt.Printf("%s leaved chat %s\n", nodeName, c.Name) + fmt.Printf("%s leaved chat %s\n", node.Name, c.Name) } index++ diff --git a/storage/storage_test.go b/storage/storage_test.go index d8d4046..a083fae 100644 --- a/storage/storage_test.go +++ b/storage/storage_test.go @@ -104,11 +104,8 @@ func Test_storage_AddNodeToChat(t *testing.T) { c, err := s.getChat(id.String(), false) assert.Nil(t, err) - addr := "127.0.0.1" - port := "8080" - nodeName := "toto" slot := uint8(1) - node := crdt.NewNodeInfos(addr, port, nodeName) + node := crdt.NewNodeInfos("127.0.0.1", "8080", "toto") node.Slot = slot numberOfSlots := c.GetSlots() @@ -121,16 +118,7 @@ func Test_storage_AddNodeToChat(t *testing.T) { assert.Equal(t, 1, len(numberOfSlots)) // Verify node - n, err := c.GetNodeBySlot(slot) - assert.Nil(t, err) - assert.Equal(t, n.Name, nodeName) - assert.Equal(t, n.Port, port) - assert.Equal(t, n.Address, addr) - assert.Equal(t, n.Slot, slot) - - // Try to get un existent node - n, err = c.GetNodeBySlot(uint8(10)) - assert.Equal(t, err.Error(), NotFoundErr.Error()) + assert.True(t, contains(slot, c.GetSlots())) } func Test_storage_RemoveNodeFromChat(t *testing.T) { @@ -142,9 +130,10 @@ func Test_storage_RemoveNodeFromChat(t *testing.T) { c, err := s.getChat(id.String(), false) assert.Nil(t, err) - node := crdt.NewNodeInfos("127.0.0.1", "8080", "toto") // Setting slot to identify active TCP connection - node.Slot = 1 + nodeSlot := uint8(1) + node := crdt.NewNodeInfos("127.0.0.1", "8080", "toto") + node.Slot = nodeSlot err = s.AddNodeToChat(node, id) assert.Nil(t, err) @@ -153,11 +142,11 @@ func Test_storage_RemoveNodeFromChat(t *testing.T) { chatId, err := uuid.Parse(c.Id.String()) assert.Nil(t, err) - err = s.RemoveNodeFromChat(node.Slot, chatId) + err = s.RemoveNodeFromChat(nodeSlot, chatId) assert.Nil(t, err) // try to remove in existent node slot - err = s.RemoveNodeFromChat(node.Slot, chatId) + err = s.RemoveNodeFromChat(nodeSlot, chatId) assert.NotNil(t, err) } @@ -174,14 +163,17 @@ func TestStorage_RemoveNodeSlotFromStorage(t *testing.T) { first, err := s.AddNewChat("first") assert.Nil(t, err) + second, err := s.AddNewChat("second") assert.Nil(t, err) - firstNode := crdt.NewNodeInfos("", "", "first") - firstNode.Slot = uint8(1) + firstNode := crdt.NewNodeInfos("127.0.0.1", "8080", "toto") + firstNodeSlot := uint8(1) + firstNode.Slot = firstNodeSlot - secondNode := crdt.NewNodeInfos("", "", "second") - secondNode.Slot = uint8(2) + secondNode := crdt.NewNodeInfos("127.0.0.1", "8080", "toto") + secondNodeSlot := uint8(2) + secondNode.Slot = secondNodeSlot err = s.AddNodeToChat(firstNode, first) assert.Nil(t, err) @@ -191,15 +183,10 @@ func TestStorage_RemoveNodeSlotFromStorage(t *testing.T) { c1, err := s.getChat(first.String(), false) assert.Nil(t, err) - _, err = c1.GetNodeBySlot(uint8(1)) - assert.Nil(t, err) - - _, err = c1.GetNodeBySlot(uint8(2)) - assert.Nil(t, err) + assert.True(t, contains(uint8(1), c1.GetSlots())) s.RemoveNodeSlotFromStorage(2) - _, err = c1.GetNodeBySlot(uint8(2)) - assert.Equal(t, err.Error(), NotFoundErr.Error()) + assert.True(t, !contains(uint8(2), c1.GetSlots())) err = s.AddNodeToChat(secondNode, first) assert.Nil(t, err) @@ -215,3 +202,16 @@ func TestStorage_RemoveNodeSlotFromStorage(t *testing.T) { assert.Equal(t, 1, len(c1.GetSlots())) assert.Equal(t, 0, len(c2.GetSlots())) } + +func contains(element uint8, elements []uint8) bool { + if len(elements) == 0 { + return false + } + + for _, e := range elements { + if e == element { + return true + } + } + return false +}