Browse Source

First implementation of Group

andrea 6 tháng trước cách đây
mục cha
commit
4045a9c705

+ 4 - 4
models/collection.go

@@ -2,10 +2,6 @@ package models
 
 import "encoding/json"
 
-type Filter struct {
-	Tags []*Tag
-}
-
 type Collection struct {
 	Meta
 
@@ -39,3 +35,7 @@ func (c *Collection) Marshal() ([]byte, error) {
 func (c *Collection) Unmarshal(data []byte) error {
 	return json.Unmarshal(data, c)
 }
+
+func (c *Collection) Create() *Collection {
+	return &Collection{}
+}

+ 9 - 0
models/filters.go

@@ -0,0 +1,9 @@
+package models
+
+type Filter struct {
+	Tags []*Tag
+}
+
+type ParticipantFilter struct {
+	Attributes map[string]string
+}

+ 29 - 0
models/group.go

@@ -1,6 +1,35 @@
 package models
 
+import (
+	"github.com/gocarina/gocsv"
+)
+
 type Group struct {
+	Meta
 	Name         string
 	Participants []*Participant
 }
+
+func (g *Group) String() string {
+	return g.Name
+}
+
+func (g *Group) GetID() string {
+	return g.ID
+}
+
+func (g *Group) SetID(id string) {
+	g.ID = id
+}
+
+func (g *Group) GetHash() string {
+	return ""
+}
+
+func (g *Group) Marshal() ([]byte, error) {
+	return gocsv.MarshalBytes(g.Participants)
+}
+
+func (g *Group) Unmarshal(data []byte) error {
+	return gocsv.UnmarshalBytes(data, g.Participants)
+}

+ 29 - 0
models/group_test.go

@@ -0,0 +1,29 @@
+package models
+
+import (
+	"github.com/remogatto/prettytest"
+)
+
+type groupTestSuite struct {
+	prettytest.Suite
+}
+
+func (t *groupTestSuite) TestMarshal() {
+	group := &Group{
+		Name: "Example group",
+		Participants: []*Participant{
+			{"123", "John", "Doe", 12345, map[string]string{"class": "1 D LIN", "age": "18"}},
+			{"456", "Jack", "Sparrow", 67890, map[string]string{"class": "1 D LIN", "age": "24"}},
+		},
+	}
+
+	expected := `id,firstname,lastname,token,attributes
+123,John,Doe,12345,"age:18,class:1 D LIN"
+456,Jack,Sparrow,67890,"age:24,class:1 D LIN"
+`
+
+	csv, err := group.Marshal()
+
+	t.Nil(err)
+	t.Equal(expected, string(csv))
+}

+ 1 - 0
models/models_test.go

@@ -16,6 +16,7 @@ func TestRunner(t *testing.T) {
 	prettytest.Run(
 		t,
 		new(testSuite),
+		new(groupTestSuite),
 	)
 }
 

+ 28 - 1
models/participant.go

@@ -4,9 +4,12 @@ import (
 	"crypto/sha256"
 	"encoding/json"
 	"fmt"
+	"sort"
 	"strings"
 )
 
+type AttributeList map[string]string
+
 type Participant struct {
 	ID string `csv:"id" gorm:"primaryKey"`
 
@@ -15,7 +18,7 @@ type Participant struct {
 
 	Token uint `csv:"token"`
 
-	Attributes map[string]string
+	Attributes AttributeList `csv:"attributes"`
 }
 
 func (p *Participant) String() string {
@@ -52,3 +55,27 @@ func (p *Participant) Marshal() ([]byte, error) {
 func (p *Participant) Unmarshal(data []byte) error {
 	return json.Unmarshal(data, p)
 }
+
+func (al AttributeList) MarshalCSV() (string, error) {
+	result := convertMapToKeyValueOrderedString(al)
+	return result, nil
+}
+
+func convertMapToKeyValueOrderedString(m map[string]string) string {
+	keys := make([]string, 0, len(m))
+	for key := range m {
+		keys = append(keys, key)
+	}
+
+	sort.Strings(keys)
+
+	var result strings.Builder
+	for _, key := range keys {
+		result.WriteString(key)
+		result.WriteString(":")
+		result.WriteString(m[key])
+		result.WriteString(",")
+	}
+
+	return strings.TrimSuffix(result.String(), ",")
+}

+ 1 - 1
store/file/collection.go

@@ -14,7 +14,7 @@ func NewCollectionFileStore(config *FileStoreConfig[*models.Collection, *store.C
 func NewDefaultCollectionFileStore() (*CollectionFileStore, error) {
 	return NewCollectionFileStore(
 		&FileStoreConfig[*models.Collection, *store.CollectionStore]{
-			FilePathConfig: FilePathConfig{DefaultBaseDir, "collection", ".json"},
+			FilePathConfig: FilePathConfig{GetDefaultCollectionsDir(), "collection", ".json"},
 			IndexDirFunc:   DefaultIndexDirFunc[*models.Collection, *store.CollectionStore],
 		},
 	)

+ 5 - 0
store/file/defaults.go

@@ -7,6 +7,7 @@ var (
 	DefaultQuizzesSubdir      = "quizzes"
 	DefaultCollectionsSubdir  = "collections"
 	DefaultParticipantsSubdir = "participants"
+	DefaultGroupsSubdir       = "groups"
 )
 
 func GetDefaultQuizzesDir() string {
@@ -20,3 +21,7 @@ func GetDefaultCollectionsDir() string {
 func GetDefaultParticipantsDir() string {
 	return filepath.Join(DefaultBaseDir, DefaultParticipantsSubdir)
 }
+
+func GetDefaultGroupsDir() string {
+	return filepath.Join(DefaultBaseDir, DefaultGroupsSubdir)
+}

+ 2 - 0
store/file/file.go

@@ -23,6 +23,8 @@ type FileStorable interface {
 
 	Marshal() ([]byte, error)
 	Unmarshal([]byte) error
+
+	Create() FileStorable
 }
 
 type Storer[T store.Storable] interface {

+ 1 - 0
store/file/file_test.go

@@ -16,5 +16,6 @@ func TestRunner(t *testing.T) {
 		new(quizTestSuite),
 		new(collectionTestSuite),
 		new(participantTestSuite),
+		new(groupTestSuite),
 	)
 }

+ 22 - 0
store/file/group.go

@@ -0,0 +1,22 @@
+package file
+
+import (
+	"git.andreafazzi.eu/andrea/probo/models"
+	"git.andreafazzi.eu/andrea/probo/store"
+)
+
+type GroupFileStore = FileStore[*models.Group, *store.Store[*models.Group]]
+
+func NewGroupFileStore(config *FileStoreConfig[*models.Group, *store.GroupStore]) (*GroupFileStore, error) {
+	return NewFileStore[*models.Group](config, store.NewStore[*models.Group]())
+}
+
+func NewDefaultGroupFileStore() (*GroupFileStore, error) {
+	return NewGroupFileStore(
+		&FileStoreConfig[*models.Group, *store.GroupStore]{
+			FilePathConfig: FilePathConfig{GetDefaultGroupsDir(), "group", ".csv"},
+			IndexDirFunc:   DefaultIndexDirFunc[*models.Group, *store.GroupStore],
+		},
+	)
+
+}

+ 80 - 0
store/file/group_test.go

@@ -0,0 +1,80 @@
+package file
+
+import (
+	"fmt"
+	"os"
+
+	"git.andreafazzi.eu/andrea/probo/models"
+	"git.andreafazzi.eu/andrea/probo/store"
+	"github.com/gocarina/gocsv"
+	"github.com/remogatto/prettytest"
+)
+
+type groupTestSuite struct {
+	prettytest.Suite
+}
+
+func (t *groupTestSuite) TestCreate() {
+	participantStore := store.NewParticipantStore()
+
+	participantStore.Create(
+		&models.Participant{
+			ID:         "1234",
+			Firstname:  "John",
+			Lastname:   "Smith",
+			Token:      111222,
+			Attributes: models.AttributeList{"class": "1 D LIN"},
+		})
+
+	participantStore.Create(
+		&models.Participant{
+			ID:         "5678",
+			Firstname:  "Jack",
+			Lastname:   "Sparrow",
+			Token:      222333,
+			Attributes: models.AttributeList{"class": "2 D LIN"},
+		})
+
+	groupStore, err := NewDefaultGroupFileStore()
+	t.Nil(err)
+
+	if !t.Failed() {
+		g := new(models.Group)
+		g.Name = "Test Group"
+
+		participantStore.FilterInGroup(g, &models.ParticipantFilter{
+			Attributes: map[string]string{"class": "1 D LIN"},
+		})
+
+		_, err = groupStore.Create(g)
+		t.Nil(err)
+
+		defer os.Remove(groupStore.GetPath(g))
+
+		participantsFromDisk, err := readGroupFromCSV(g.GetID())
+		t.Nil(err)
+		if !t.Failed() {
+			t.Equal("Smith", participantsFromDisk[0].Lastname)
+		}
+	}
+}
+
+func readGroupFromCSV(groupID string) ([]*models.Participant, error) {
+	// Build the path to the CSV file
+	csvPath := fmt.Sprintf("testdata/groups/group_%s.csv", groupID)
+
+	// Open the CSV file
+	file, err := os.Open(csvPath)
+	if err != nil {
+		return nil, fmt.Errorf("failed to open CSV file: %w", err)
+	}
+	defer file.Close()
+
+	// Parse the CSV file
+	var participants []*models.Participant
+	if err := gocsv.UnmarshalFile(file, &participants); err != nil {
+		return nil, fmt.Errorf("failed to parse CSV file: %w", err)
+	}
+
+	return participants, nil
+}

+ 12 - 3
store/file/participant.go

@@ -5,8 +5,17 @@ import (
 	"git.andreafazzi.eu/andrea/probo/store"
 )
 
-type ParticipantFileStore = FileStore[*models.Participant, *store.Store[*models.Participant]]
+type ParticipantFileStore = FileStore[*models.Participant, *store.ParticipantStore]
 
-func NewParticipantFileStore(config *FileStoreConfig[*models.Participant, *store.Store[*models.Participant]]) (*ParticipantFileStore, error) {
-	return NewFileStore[*models.Participant](config, store.NewStore[*models.Participant]())
+func NewParticipantFileStore(config *FileStoreConfig[*models.Participant, *store.ParticipantStore]) (*ParticipantFileStore, error) {
+	return NewFileStore[*models.Participant, *store.ParticipantStore](config, store.NewParticipantStore())
+}
+
+func NewParticipantDefaultFileStore() (*ParticipantFileStore, error) {
+	return NewParticipantFileStore(
+		&FileStoreConfig[*models.Participant, *store.ParticipantStore]{
+			FilePathConfig: FilePathConfig{GetDefaultParticipantsDir(), "participant", ".json"},
+			IndexDirFunc:   DefaultIndexDirFunc[*models.Participant, *store.ParticipantStore],
+		},
+	)
 }

+ 1 - 1
store/file/quiz.go

@@ -65,7 +65,7 @@ func DefaultQuizIndexDirFunc(s *QuizFileStore) error {
 
 		var errQuizAlreadyPresent *store.ErrQuizAlreadyPresent
 
-		mEntity, err := s.Create(entity)
+		mEntity, err := s.Storer.Create(entity)
 		if err != nil && !errors.As(err, &errQuizAlreadyPresent) {
 			return err
 		}

+ 183 - 164
store/file/quiz_test.go

@@ -5,6 +5,8 @@ import (
 	"os"
 	"path/filepath"
 
+	"git.andreafazzi.eu/andrea/probo/models"
+	"git.andreafazzi.eu/andrea/probo/store"
 	"github.com/remogatto/prettytest"
 )
 
@@ -35,167 +37,184 @@ func (t *quizTestSuite) TestReadAll() {
 	}
 }
 
-// func (t *quizTestSuite) TestCreate() {
-// 	store, err := NewDefaultQuizFileStore()
-// 	t.Nil(err)
-
-// 	if !t.Failed() {
-// 		quiz, err := store.Create(
-// 			&models.Quiz{
-// 				Question: &models.Question{Text: "Newly created question text with #tag1 #tag2."},
-// 				Answers: []*models.Answer{
-// 					{Text: "Answer 1"},
-// 					{Text: "Answer 2"},
-// 					{Text: "Answer 3"},
-// 					{Text: "Answer 4"},
-// 				},
-// 				CorrectPos: 0,
-// 			})
-// 		t.Nil(err)
-// 		t.Equal(2, len(quiz.Tags))
-
-// 		if !t.Failed() {
-// 			path := store.GetPath(quiz)
-// 			t.True(path != "", "Path should not be empty.")
-
-// 			exists, err := os.Stat(path)
-// 			t.Nil(err)
-
-// 			if !t.Failed() {
-// 				t.True(exists != nil, "The new quiz file was not created.")
-
-// 				if !t.Failed() {
-// 					quizFromDisk, err := readQuizFromDisk(path)
-// 					defer os.Remove(path)
-
-// 					quizFromDisk.Correct = quiz.Answers[0]
-// 					quizFromDisk.Tags = quiz.Tags
-
-// 					t.Nil(err)
-
-// 					if !t.Failed() {
-// 						t.Equal(quizFromDisk.Question.Text, quiz.Question.Text)
-// 						for i, a := range quizFromDisk.Answers {
-// 							t.Equal(a.Text, quiz.Answers[i].Text)
-// 						}
-// 						for i, tag := range quizFromDisk.Tags {
-// 							t.Equal(tag.Name, quiz.Tags[i].Name)
-// 						}
-// 					}
-// 				}
-// 			}
-
-// 		}
-// 	}
-// }
-
-// func (t *quizTestSuite) TestDelete() {
-// 	store, err := NewDefaultQuizFileStore()
-// 	t.Nil(err)
-
-// 	if !t.Failed() {
-// 		quiz, err := store.Create(
-// 			&models.Quiz{
-// 				Question: &models.Question{Text: "This quiz should be deleted."},
-// 				Answers: []*models.Answer{
-// 					{Text: "Answer 1"},
-// 					{Text: "Answer 2"},
-// 					{Text: "Answer 3"},
-// 					{Text: "Answer 4"},
-// 				},
-// 				CorrectPos: 0,
-// 			})
-// 		t.Nil(err)
-// 		if !t.Failed() {
-// 			path := store.GetPath(quiz)
-// 			_, err := store.Delete(quiz.ID)
-// 			t.Nil(err, fmt.Sprintf("Quiz should be deleted without errors: %v", err))
-// 			if !t.Failed() {
-// 				_, err := os.Stat(path)
-// 				t.Not(t.Nil(err))
-
-// 			}
-
-// 		}
-// 	}
-// }
-
-// func (t *quizTestSuite) TestUpdate() {
-// 	store, err := NewDefaultQuizFileStore()
-// 	t.Nil(err)
-
-// 	if !t.Failed() {
-// 		quiz, err := store.Create(
-// 			&models.Quiz{
-// 				Question: &models.Question{Text: "Newly created question text with #tag1 #tag2."},
-// 				Answers: []*models.Answer{
-// 					{Text: "Answer 1"},
-// 					{Text: "Answer 2"},
-// 					{Text: "Answer 3"},
-// 					{Text: "Answer 4"},
-// 				},
-// 				CorrectPos: 0,
-// 			})
-// 		t.Nil(err)
-
-// 		_, err = store.Update(&models.Quiz{
-// 			Question: &models.Question{Text: "Newly created question text with #tag1 #tag2 #tag3."},
-// 			Answers: []*models.Answer{
-// 				{Text: "Answer 1"},
-// 				{Text: "Answer 2"},
-// 				{Text: "Answer 3"},
-// 				{Text: "Answer 4"},
-// 			},
-// 			CorrectPos: 1,
-// 		}, quiz.ID)
-
-// 		t.Nil(err)
-
-// 		updatedQuizFromMemory, err := store.Read(quiz.ID)
-// 		t.Equal(len(updatedQuizFromMemory.Tags), 3)
-// 		t.Equal("Answer 2", updatedQuizFromMemory.Correct.Text)
-
-// 		defer os.Remove(store.GetPath(quiz))
-
-// 	}
-// }
-
-// func (t *quizTestSuite) TestAutowriteHeader() {
-// 	store, err := NewDefaultQuizFileStore()
-// 	t.Nil(err)
-
-// 	if !t.Failed() {
-
-// 		meta, err := readQuizHeader(filepath.Join(store.Dir, "quiz_5.md"))
-// 		t.Nil(err)
-
-// 		if !t.Failed() {
-// 			t.Not(t.Nil(meta))
-
-// 			if !t.Failed() {
-// 				t.True(meta.ID != "", "ID should not be empty")
-
-// 				if !t.Failed() {
-// 					_, err = removeQuizHeader(filepath.Join(store.Dir, "quiz_5.md"))
-// 					t.True(err == nil)
-// 				}
-// 			}
-// 		}
-// 	}
-// }
-
-// func readQuizFromDisk(path string) (*models.Quiz, error) {
-// 	content, err := os.ReadFile(path)
-// 	if err != nil {
-// 		return nil, err
-// 	}
-
-// 	result := new(models.Quiz)
-
-// 	err = result.Unmarshal(content)
-// 	if err != nil {
-// 		return nil, err
-// 	}
-
-// 	return result, nil
-// }
+func (t *quizTestSuite) TestCreate() {
+	store, err := NewQuizFileStore(
+		&FileStoreConfig[*models.Quiz, *store.QuizStore]{
+			FilePathConfig:  FilePathConfig{GetDefaultQuizzesDir(), "quiz", ".md"},
+			IndexDirFunc:    DefaultQuizIndexDirFunc,
+			NoIndexOnCreate: true,
+		},
+	)
+	t.Nil(err)
+
+	if !t.Failed() {
+		quiz, err := store.Create(
+			&models.Quiz{
+				Question: &models.Question{Text: "Newly created question text with #tag1 #tag2."},
+				Answers: []*models.Answer{
+					{Text: "Answer 1"},
+					{Text: "Answer 2"},
+					{Text: "Answer 3"},
+					{Text: "Answer 4"},
+				},
+				CorrectPos: 0,
+			})
+		t.Nil(err)
+		t.Equal(2, len(quiz.Tags))
+
+		if !t.Failed() {
+			path := store.GetPath(quiz)
+			t.True(path != "", "Path should not be empty.")
+
+			exists, err := os.Stat(path)
+			t.Nil(err)
+
+			if !t.Failed() {
+				t.True(exists != nil, "The new quiz file was not created.")
+
+				if !t.Failed() {
+					quizFromDisk, err := readQuizFromDisk(path)
+					defer os.Remove(path)
+
+					quizFromDisk.Correct = quiz.Answers[0]
+					quizFromDisk.Tags = quiz.Tags
+
+					t.Nil(err)
+
+					if !t.Failed() {
+						t.Equal(quizFromDisk.Question.Text, quiz.Question.Text)
+						for i, a := range quizFromDisk.Answers {
+							t.Equal(a.Text, quiz.Answers[i].Text)
+						}
+						for i, tag := range quizFromDisk.Tags {
+							t.Equal(tag.Name, quiz.Tags[i].Name)
+						}
+					}
+				}
+			}
+
+		}
+	}
+}
+
+func (t *quizTestSuite) TestDelete() {
+	store, err := NewQuizFileStore(
+		&FileStoreConfig[*models.Quiz, *store.QuizStore]{
+			FilePathConfig:  FilePathConfig{GetDefaultQuizzesDir(), "quiz", ".md"},
+			IndexDirFunc:    DefaultQuizIndexDirFunc,
+			NoIndexOnCreate: true,
+		},
+	)
+	t.Nil(err)
+
+	if !t.Failed() {
+		quiz, err := store.Create(
+			&models.Quiz{
+				Question: &models.Question{Text: "This quiz should be deleted."},
+				Answers: []*models.Answer{
+					{Text: "Answer 1"},
+					{Text: "Answer 2"},
+					{Text: "Answer 3"},
+					{Text: "Answer 4"},
+				},
+				CorrectPos: 0,
+			})
+		t.Nil(err)
+		if !t.Failed() {
+			path := store.GetPath(quiz)
+			_, err := store.Delete(quiz.ID)
+			t.Nil(err, fmt.Sprintf("Quiz should be deleted without errors: %v", err))
+			if !t.Failed() {
+				_, err := os.Stat(path)
+				t.Not(t.Nil(err))
+
+			}
+
+		}
+	}
+}
+
+func (t *quizTestSuite) TestUpdate() {
+	store, err := NewQuizFileStore(
+		&FileStoreConfig[*models.Quiz, *store.QuizStore]{
+			FilePathConfig:  FilePathConfig{GetDefaultQuizzesDir(), "quiz", ".md"},
+			IndexDirFunc:    DefaultQuizIndexDirFunc,
+			NoIndexOnCreate: true,
+		},
+	)
+
+	t.Nil(err)
+
+	if !t.Failed() {
+		quiz, err := store.Create(
+			&models.Quiz{
+				Question: &models.Question{Text: "Newly created question text with #tag1 #tag2."},
+				Answers: []*models.Answer{
+					{Text: "Answer 1"},
+					{Text: "Answer 2"},
+					{Text: "Answer 3"},
+					{Text: "Answer 4"},
+				},
+				CorrectPos: 0,
+			})
+		t.Nil(err)
+
+		_, err = store.Update(&models.Quiz{
+			Question: &models.Question{Text: "Newly created question text with #tag1 #tag2 #tag3."},
+			Answers: []*models.Answer{
+				{Text: "Answer 1"},
+				{Text: "Answer 2"},
+				{Text: "Answer 3"},
+				{Text: "Answer 4"},
+			},
+			CorrectPos: 1,
+		}, quiz.ID)
+
+		t.Nil(err)
+
+		updatedQuizFromMemory, err := store.Read(quiz.ID)
+		t.Equal(len(updatedQuizFromMemory.Tags), 3)
+		t.Equal("Answer 2", updatedQuizFromMemory.Correct.Text)
+
+		defer os.Remove(store.GetPath(quiz))
+
+	}
+}
+
+func (t *quizTestSuite) TestAutowriteHeader() {
+	store, err := NewDefaultQuizFileStore()
+	t.Nil(err)
+
+	if !t.Failed() {
+		meta, err := readQuizHeader(filepath.Join(store.Dir, "quiz_5.md"))
+		t.Nil(err)
+		if !t.Failed() {
+			t.Not(t.Nil(meta))
+
+			if !t.Failed() {
+				t.True(meta.ID != "", "ID should not be empty")
+
+				if !t.Failed() {
+					_, err = removeQuizHeader(filepath.Join(store.Dir, "quiz_5.md"))
+					t.True(err == nil)
+				}
+			}
+		}
+	}
+}
+
+func readQuizFromDisk(path string) (*models.Quiz, error) {
+	content, err := os.ReadFile(path)
+	if err != nil {
+		return nil, err
+	}
+
+	result := new(models.Quiz)
+
+	err = result.Unmarshal(content)
+	if err != nil {
+		return nil, err
+	}
+
+	return result, nil
+}

+ 2 - 0
store/file/testdata/groups/group_96a300bb-0b29-4b32-93cf-5bcde7fcef61.csv

@@ -0,0 +1,2 @@
+id,firstname,lastname,token,attributes
+1234,John,Smith,111222,class:1 D LIN

+ 5 - 0
store/group.go

@@ -0,0 +1,5 @@
+package store
+
+import "git.andreafazzi.eu/andrea/probo/models"
+
+type GroupStore = Store[*models.Group]

+ 0 - 124
store/memory/collection.go

@@ -1,124 +0,0 @@
-package memory
-
-import (
-	"errors"
-	"fmt"
-
-	"git.andreafazzi.eu/andrea/probo/client"
-	"git.andreafazzi.eu/andrea/probo/models"
-	"github.com/google/uuid"
-)
-
-func (s *MemoryProboCollectorStore) getCollectionFromID(id string) *models.Collection {
-	s.lock.RLock()
-	defer s.lock.RUnlock()
-
-	collection, ok := s.collections[id]
-	if ok {
-		return collection
-	}
-
-	return nil
-}
-
-func (s *MemoryProboCollectorStore) ReadAllCollections() ([]*models.Collection, error) {
-	result := make([]*models.Collection, 0)
-	for id := range s.collections {
-		if collection := s.getCollectionFromID(id); collection != nil {
-			result = append(result, collection)
-		}
-	}
-	return result, nil
-}
-
-func (s *MemoryProboCollectorStore) CreateCollection(r *client.CreateUpdateCollectionRequest) (*models.Collection, error) {
-	q, _, err := s.createOrUpdateCollection(r, "")
-	return q, err
-}
-
-func (s *MemoryProboCollectorStore) UpdateCollection(r *client.CreateUpdateCollectionRequest, id string) (*models.Collection, bool, error) {
-	return s.createOrUpdateCollection(r, id)
-}
-
-func (s *MemoryProboCollectorStore) ReadCollectionByID(id string) (*models.Collection, error) {
-	if id == "" {
-		return nil, errors.New("ID should not be an empty string!")
-	}
-	collection := s.getCollectionFromID(id)
-	if collection == nil {
-		return nil, fmt.Errorf("Collection ID %v not found in the store", id)
-	}
-	return collection, nil
-}
-
-func (s *MemoryProboCollectorStore) DeleteCollection(r *client.DeleteCollectionRequest) (*models.Collection, error) {
-	return s.deleteCollection(r.ID)
-}
-
-func (s *MemoryProboCollectorStore) deleteCollection(id string) (*models.Collection, error) {
-	s.lock.Lock()
-	defer s.lock.Unlock()
-
-	collection := s.collections[id]
-	if collection == nil {
-		return nil, fmt.Errorf("Trying to delete a collection that doesn't exist in memory (ID: %v)", id)
-	}
-
-	delete(s.collections, id)
-
-	return collection, nil
-}
-
-func (s *MemoryProboCollectorStore) createOrUpdateCollection(r *client.CreateUpdateCollectionRequest, id string) (*models.Collection, bool, error) {
-	var collection *models.Collection
-
-	if r.Collection == nil {
-		return nil, false, errors.New("A request was made passing a nil collection object")
-	}
-
-	if id != "" { // we're updating a collection
-		collection = s.getCollectionFromID(id)
-		if collection == nil { // Quiz is not present in the store
-			return nil, false, fmt.Errorf("Collection ID %v doesn't exist in the store!", id)
-		}
-	} else {
-		id = uuid.New().String()
-		collection = new(models.Collection)
-	}
-
-	collection.Name = r.Collection.Name
-	collection.Query = r.Collection.Query
-
-	collection.Quizzes = s.query(collection.Query)
-
-	return s.createCollectionFromID(id, collection), true, nil
-}
-
-func (s *MemoryProboCollectorStore) query(query string) []*models.Quiz {
-	s.lock.Lock()
-	defer s.lock.Unlock()
-
-	result := make([]*models.Quiz, 0)
-
-	for _, quiz := range s.quizzes {
-		for _, tag := range quiz.Tags {
-			if query == tag.Name {
-				result = append(result, quiz)
-				break
-			}
-		}
-	}
-
-	return result
-}
-
-func (s *MemoryProboCollectorStore) createCollectionFromID(id string, collection *models.Collection) *models.Collection {
-	s.lock.Lock()
-	defer s.lock.Unlock()
-
-	collection.ID = id
-
-	s.collections[id] = collection
-
-	return collection
-}

+ 0 - 98
store/memory/collection_test.go

@@ -1,98 +0,0 @@
-package memory
-
-import (
-	"fmt"
-
-	"git.andreafazzi.eu/andrea/probo/client"
-	"git.andreafazzi.eu/andrea/probo/hasher/sha256"
-	"github.com/remogatto/prettytest"
-)
-
-type collectionTestSuite struct {
-	prettytest.Suite
-}
-
-func (t *collectionTestSuite) TestUpdateCollection() {
-	store := NewMemoryProboCollectorStore(
-		sha256.NewDefault256Hasher(sha256.DefaultSHA256HashingFn),
-	)
-
-	quiz_1, _ := store.CreateQuiz(
-		&client.CreateUpdateQuizRequest{
-			Quiz: &client.Quiz{
-				Question: &client.Question{Text: "Question text with #tag1."},
-				Answers: []*client.Answer{
-					{Text: "Answer 1", Correct: true},
-					{Text: "Answer 2", Correct: false},
-					{Text: "Answer 3", Correct: false},
-					{Text: "Answer 4", Correct: false},
-				},
-			},
-		})
-
-	quiz_2, _ := store.CreateQuiz(
-		&client.CreateUpdateQuizRequest{
-			Quiz: &client.Quiz{
-				Question: &client.Question{Text: "Another question text with #tag1."},
-				Answers: []*client.Answer{
-					{Text: "Answer 1", Correct: true},
-					{Text: "Answer 2", Correct: false},
-					{Text: "Answer 3", Correct: false},
-					{Text: "Answer 4", Correct: false},
-				},
-			},
-		})
-
-	collection, _ := store.CreateCollection(
-		&client.CreateUpdateCollectionRequest{
-			Collection: &client.Collection{
-				Name: "MyCollection",
-			},
-		})
-
-	updatedCollection, updated, err := store.UpdateCollection(
-		&client.CreateUpdateCollectionRequest{
-			Collection: &client.Collection{
-				Name:  "MyUpdatedCollection",
-				Query: "#tag1",
-			},
-		}, collection.ID)
-
-	t.Nil(err, fmt.Sprintf("The update returned an error: %v", err))
-
-	if !t.Failed() {
-		t.True(updated)
-		t.Equal("MyUpdatedCollection", updatedCollection.Name)
-		t.True(len(updatedCollection.Quizzes) == 2)
-		if !t.Failed() {
-			count := 0
-			for _, q := range updatedCollection.Quizzes {
-				if quiz_1.ID == q.ID || quiz_2.ID == q.ID {
-					count++
-				}
-			}
-			t.Equal(2, count)
-		}
-	}
-}
-
-func (t *collectionTestSuite) TestDeleteCollection() {
-	store := NewMemoryProboCollectorStore(
-		sha256.NewDefault256Hasher(sha256.DefaultSHA256HashingFn),
-	)
-	collection, _ := store.CreateCollection(
-		&client.CreateUpdateCollectionRequest{
-			Collection: &client.Collection{
-				Name:  "Collection to be deleted",
-				Query: "#tag1",
-			},
-		})
-
-	deletedCollection, err := store.DeleteCollection(&client.DeleteCollectionRequest{ID: collection.ID})
-
-	t.Equal(collection.ID, deletedCollection.ID, "Returned deleted collection ID should be equal to the request")
-	t.Nil(err, fmt.Sprintf("The update returned an error: %v", err))
-
-	_, err = store.ReadCollectionByID(deletedCollection.ID)
-	t.True(err != nil, "Reading a non existent quiz should return an error")
-}

+ 0 - 48
store/memory/memory.go

@@ -1,48 +0,0 @@
-package memory
-
-import (
-	"sync"
-
-	"git.andreafazzi.eu/andrea/probo/hasher"
-	"git.andreafazzi.eu/andrea/probo/models"
-	"git.andreafazzi.eu/andrea/probo/store"
-)
-
-type Store[T Storable] struct {
-	ids    map[string]T
-	hashes map[string]T
-
-	// A mutex is used to synchronize read/write access to the map
-	lock sync.RWMutex
-}
-
-func NewStore[T store.Storable]() *Store[T] {
-	store := new(Store[T])
-
-	store.ids = make(map[string]T)
-
-	return store
-}
-
-type QuizStore struct {
-	*Store[*Quiz]
-
-	questions *Store[*Question]
-	answers   *Store[*Answer]
-}
-
-func NewMemoryProboCollectorStore(hasher hasher.Hasher) *MemoryProboCollectorStore {
-	s := new(MemoryProboCollectorStore)
-
-	s.hasher = hasher
-
-	s.questionsHashes = make(map[string]*models.Question)
-	s.answersHashes = make(map[string]*models.Answer)
-	s.quizzesHashes = make(map[string]*models.Quiz)
-
-	s.quizzes = make(map[string]*models.Quiz)
-	s.collections = make(map[string]*models.Collection)
-	s.participants = make(map[string]*models.Participant)
-
-	return s
-}

+ 0 - 143
store/memory/memory_test.go

@@ -1,143 +0,0 @@
-package memory
-
-import (
-	"fmt"
-	"reflect"
-	"testing"
-
-	"git.andreafazzi.eu/andrea/probo/client"
-	"git.andreafazzi.eu/andrea/probo/hasher/sha256"
-	"github.com/remogatto/prettytest"
-)
-
-type testSuite struct {
-	prettytest.Suite
-}
-
-func TestRunner(t *testing.T) {
-	prettytest.Run(
-		t,
-		new(testSuite),
-		new(collectionTestSuite),
-		new(participantTestSuite),
-	)
-}
-
-func (t *testSuite) TestReadQuizByHash() {
-	store := NewMemoryProboCollectorStore(
-		sha256.NewDefault256Hasher(sha256.DefaultSHA256HashingFn),
-	)
-	quiz, _ := store.CreateQuiz(
-		&client.CreateUpdateQuizRequest{
-			Quiz: &client.Quiz{
-				Question: &client.Question{Text: "Newly created question text."},
-				Answers: []*client.Answer{
-					{Text: "Answer 1", Correct: true},
-					{Text: "Answer 2", Correct: false},
-					{Text: "Answer 3", Correct: false},
-					{Text: "Answer 4", Correct: false},
-				},
-			},
-		})
-	quizFromMemory, err := store.ReadQuizByHash(quiz.Hash)
-	t.Nil(err, "Quiz should be found in the store")
-	if !t.Failed() {
-		t.True(reflect.DeepEqual(quizFromMemory, quiz), "Quiz should be equal")
-	}
-
-}
-
-func (t *testSuite) TestParseTextForTags() {
-	store := NewMemoryProboCollectorStore(
-		sha256.NewDefault256Hasher(sha256.DefaultSHA256HashingFn),
-	)
-	quiz, _ := store.CreateQuiz(
-		&client.CreateUpdateQuizRequest{
-			Quiz: &client.Quiz{
-				Question: &client.Question{Text: "Newly created question text with #tag1."},
-				Answers: []*client.Answer{
-					{Text: "Answer 1", Correct: true},
-					{Text: "Answer 2 with #tag2", Correct: false},
-					{Text: "Answer 3", Correct: false},
-					{Text: "Answer 4", Correct: false},
-				},
-			},
-		})
-	quizFromMemory, err := store.ReadQuizByHash(quiz.Hash)
-	t.Nil(err, "Quiz should be found in the store")
-	if !t.Failed() {
-		t.True(len(quizFromMemory.Tags) == 2, "Two tags should be present.")
-		t.Equal("#tag1", quizFromMemory.Tags[0].Name)
-		t.Equal("#tag2", quizFromMemory.Tags[1].Name)
-	}
-
-}
-
-func (t *testSuite) TestUpdateQuiz() {
-	store := NewMemoryProboCollectorStore(
-		sha256.NewDefault256Hasher(sha256.DefaultSHA256HashingFn),
-	)
-	quiz, _ := store.CreateQuiz(
-		&client.CreateUpdateQuizRequest{
-			Quiz: &client.Quiz{
-				Question: &client.Question{Text: "Newly created question text."},
-				Answers: []*client.Answer{
-					{Text: "Answer 1", Correct: true},
-					{Text: "Answer 2", Correct: false},
-					{Text: "Answer 3", Correct: false},
-					{Text: "Answer 4", Correct: false},
-				},
-			},
-		})
-
-	createdQuizHash := quiz.Hash
-
-	updatedQuiz, updated, err := store.UpdateQuiz(
-		&client.CreateUpdateQuizRequest{
-			Quiz: &client.Quiz{
-				Question: &client.Question{Text: "Updated question text."},
-				Answers: []*client.Answer{
-					{Text: "Answer 1", Correct: true},
-					{Text: "Updated Answer 2", Correct: false},
-					{Text: "Answer 3", Correct: false},
-					{Text: "Answer 4", Correct: false},
-				},
-			},
-		}, quiz.ID)
-
-	t.Nil(err, fmt.Sprintf("The update returned an error: %v", err))
-
-	if !t.Failed() {
-		t.True(updated)
-		t.True(createdQuizHash != updatedQuiz.Hash, "The two hashes should not be equal.")
-		t.Equal(4, len(updatedQuiz.Answers))
-		t.Equal("Updated question text.", updatedQuiz.Question.Text)
-		t.Equal("Updated Answer 2", updatedQuiz.Answers[1].Text)
-	}
-}
-
-func (t *testSuite) TestDeleteQuiz() {
-	store := NewMemoryProboCollectorStore(
-		sha256.NewDefault256Hasher(sha256.DefaultSHA256HashingFn),
-	)
-	quiz, _ := store.CreateQuiz(
-		&client.CreateUpdateQuizRequest{
-			Quiz: &client.Quiz{
-				Question: &client.Question{Text: "This test should be removed."},
-				Answers: []*client.Answer{
-					{Text: "Answer 1", Correct: true},
-					{Text: "Answer 2", Correct: false},
-					{Text: "Answer 3", Correct: false},
-					{Text: "Answer 4", Correct: false},
-				},
-			},
-		})
-
-	deletedQuiz, err := store.DeleteQuiz(&client.DeleteQuizRequest{ID: quiz.ID})
-
-	t.Equal(quiz.ID, deletedQuiz.ID, "Returned deleted quiz ID should be equal to the request")
-	t.Nil(err, fmt.Sprintf("The update returned an error: %v", err))
-
-	_, err = store.ReadQuizByHash(deletedQuiz.Hash)
-	t.True(err != nil, "Reading a non existent quiz should return an error")
-}

+ 0 - 108
store/memory/participant.go

@@ -1,108 +0,0 @@
-package memory
-
-import (
-	"errors"
-	"fmt"
-
-	"git.andreafazzi.eu/andrea/probo/client"
-	"git.andreafazzi.eu/andrea/probo/models"
-	"github.com/google/uuid"
-)
-
-func (s *MemoryProboCollectorStore) ReadAllParticipants() ([]*models.Participant, error) {
-	result := make([]*models.Participant, 0)
-	for id := range s.participants {
-		if participant := s.getParticipantFromID(id); participant != nil {
-			result = append(result, participant)
-		}
-	}
-	return result, nil
-}
-
-func (s *MemoryProboCollectorStore) getParticipantFromID(id string) *models.Participant {
-	s.lock.RLock()
-	defer s.lock.RUnlock()
-
-	participant, ok := s.participants[id]
-	if ok {
-		return participant
-	}
-
-	return nil
-}
-
-func (s *MemoryProboCollectorStore) CreateParticipant(r *client.CreateUpdateParticipantRequest) (*models.Participant, error) {
-	q, _, err := s.createOrUpdateParticipant(r, "")
-	return q, err
-}
-
-func (s *MemoryProboCollectorStore) UpdateParticipant(r *client.CreateUpdateParticipantRequest, id string) (*models.Participant, bool, error) {
-	return s.createOrUpdateParticipant(r, id)
-}
-
-func (s *MemoryProboCollectorStore) ReadParticipantByID(id string) (*models.Participant, error) {
-	if id == "" {
-		return nil, errors.New("ID should not be an empty string!")
-	}
-	participant := s.getParticipantFromID(id)
-	if participant == nil {
-		return nil, fmt.Errorf("Participant ID %v not found in the store", id)
-	}
-	return participant, nil
-}
-
-func (s *MemoryProboCollectorStore) DeleteParticipant(r *client.DeleteParticipantRequest) (*models.Participant, error) {
-	return s.deleteParticipant(r.ID)
-}
-
-func (s *MemoryProboCollectorStore) deleteParticipant(id string) (*models.Participant, error) {
-	s.lock.Lock()
-	defer s.lock.Unlock()
-
-	participant := s.participants[id]
-	if participant == nil {
-		return nil, fmt.Errorf("Trying to delete a participant that doesn't exist in memory (ID: %v)", id)
-	}
-
-	delete(s.participants, id)
-
-	return participant, nil
-}
-
-func (s *MemoryProboCollectorStore) createOrUpdateParticipant(r *client.CreateUpdateParticipantRequest, id string) (*models.Participant, bool, error) {
-	var participant *models.Participant
-
-	if r.Participant == nil {
-		return nil, false, errors.New("A request was made passing a nil participant object")
-	}
-
-	if id != "" { // we're updating a participant
-		participant = s.getParticipantFromID(id)
-		if participant == nil { // Participant is not present in the store
-			return nil, false, fmt.Errorf("Participant ID %v doesn't exist in the store!", id)
-		}
-	} else {
-		id = uuid.New().String()
-		participant = new(models.Participant)
-	}
-
-	participant.Attributes = make(map[string]string)
-
-	participant.Firstname = r.Participant.Firstname
-	participant.Lastname = r.Participant.Lastname
-	participant.Token = r.Participant.Token
-	participant.Attributes = r.Participant.Attributes
-
-	return s.createParticipantFromID(id, participant), true, nil
-}
-
-func (s *MemoryProboCollectorStore) createParticipantFromID(id string, participant *models.Participant) *models.Participant {
-	s.lock.Lock()
-	defer s.lock.Unlock()
-
-	participant.ID = id
-
-	s.participants[id] = participant
-
-	return participant
-}

+ 0 - 62
store/memory/participant_test.go

@@ -1,62 +0,0 @@
-package memory
-
-import (
-	"fmt"
-
-	"git.andreafazzi.eu/andrea/probo/client"
-	"git.andreafazzi.eu/andrea/probo/hasher/sha256"
-	"github.com/remogatto/prettytest"
-)
-
-type participantTestSuite struct {
-	prettytest.Suite
-}
-
-func (t *participantTestSuite) TestUpdateParticipant() {
-	store := NewMemoryProboCollectorStore(
-		sha256.NewDefault256Hasher(sha256.DefaultSHA256HashingFn),
-	)
-
-	participant, _ := store.CreateParticipant(&client.CreateUpdateParticipantRequest{
-		Participant: &client.Participant{
-			Firstname: "John",
-			Lastname:  "Doe",
-			Token:     1234,
-		},
-	})
-
-	updatedParticipant, updated, err := store.UpdateParticipant(&client.CreateUpdateParticipantRequest{
-		Participant: &client.Participant{
-			Firstname: "Jack",
-			Lastname:  "Smith",
-		},
-	}, participant.ID)
-
-	t.Nil(err, fmt.Sprintf("The update returned an error: %v", err))
-
-	if !t.Failed() {
-		t.True(updated)
-		t.Equal("Jack", updatedParticipant.Firstname)
-	}
-}
-
-func (t *participantTestSuite) TestDeleteParticipant() {
-	store := NewMemoryProboCollectorStore(
-		sha256.NewDefault256Hasher(sha256.DefaultSHA256HashingFn),
-	)
-	participant, _ := store.CreateParticipant(
-		&client.CreateUpdateParticipantRequest{
-			Participant: &client.Participant{
-				Firstname: "Jack",
-				Lastname:  "Smith",
-			},
-		})
-
-	deletedParticipant, err := store.DeleteParticipant(&client.DeleteParticipantRequest{ID: participant.ID})
-
-	t.Equal(participant.ID, deletedParticipant.ID, "Returned deleted participant ID should be equal to the request")
-	t.Nil(err, fmt.Sprintf("The update returned an error: %v", err))
-
-	_, err = store.ReadParticipantByID(deletedParticipant.ID)
-	t.True(err != nil, "Reading a non existent participant should return an error")
-}

+ 0 - 246
store/memory/quiz.go

@@ -1,246 +0,0 @@
-package memory
-
-import (
-	"errors"
-	"fmt"
-	"strings"
-
-	"git.andreafazzi.eu/andrea/probo/client"
-	"git.andreafazzi.eu/andrea/probo/models"
-	"github.com/google/uuid"
-)
-
-func (s *MemoryProboCollectorStore) ReadAllQuizzes() ([]*models.Quiz, error) {
-	result := make([]*models.Quiz, 0)
-	for id := range s.quizzes {
-		if quiz := s.getQuizFromID(id); quiz != nil {
-			result = append(result, quiz)
-		}
-	}
-	return result, nil
-}
-
-func (s *MemoryProboCollectorStore) ReadQuizByID(id string) (*models.Quiz, error) {
-	quiz := s.getQuizFromID(id)
-	if quiz == nil {
-		return nil, fmt.Errorf("Quiz with ID %s was not found in the store.", id)
-	}
-	return quiz, nil
-}
-
-func (s *MemoryProboCollectorStore) ReadQuizByHash(hash string) (*models.Quiz, error) {
-	quiz := s.getQuizFromHash(hash)
-	if quiz == nil {
-		return nil, fmt.Errorf("Quiz with hash %s was not found in the store.", hash)
-	}
-	return quiz, nil
-}
-
-func (s *MemoryProboCollectorStore) CreateQuiz(r *client.CreateUpdateQuizRequest) (*models.Quiz, error) {
-	q, _, err := s.createOrUpdateQuiz(r, "")
-	return q, err
-}
-
-func (s *MemoryProboCollectorStore) UpdateQuiz(r *client.CreateUpdateQuizRequest, id string) (*models.Quiz, bool, error) {
-	return s.createOrUpdateQuiz(r, id)
-}
-
-func (s *MemoryProboCollectorStore) DeleteQuiz(r *client.DeleteQuizRequest) (*models.Quiz, error) {
-	return s.deleteQuiz(r.ID)
-}
-
-func (s *MemoryProboCollectorStore) CalculateQuizHash(quiz *client.Quiz) string {
-	hashes := s.hasher.QuizHashes(quiz)
-	return hashes[len(hashes)-1]
-}
-
-func (s *MemoryProboCollectorStore) parseTextForTags(text string, tags *[]*models.Tag) string {
-
-	// Trim the following chars
-	trimChars := "*:.,/\\@()[]{}<>"
-
-	// Split the text into words
-	words := strings.Fields(text)
-
-	for _, word := range words {
-		// If the word starts with '#', it is considered as a tag
-		if strings.HasPrefix(word, "#") {
-			// Check if the tag already exists in the tags slice
-			exists := false
-			for _, tag := range *tags {
-				if tag.Name == word {
-					exists = true
-					break
-				}
-			}
-
-			// If the tag does not exist in the tags slice, add it
-			if !exists {
-				*tags = append(*tags, &models.Tag{Name: strings.TrimRight(word, trimChars)})
-			}
-		}
-	}
-
-	return text
-}
-
-func (s *MemoryProboCollectorStore) createOrUpdateQuiz(r *client.CreateUpdateQuizRequest, id string) (*models.Quiz, bool, error) {
-	if r.Quiz == nil {
-		return nil, false, errors.New("A request was made passing a nil quiz object")
-	}
-	hashes := s.hasher.QuizHashes(r.Quiz)
-	quizHash := hashes[len(hashes)-1]
-
-	quiz := s.getQuizFromHash(quizHash)
-	if quiz != nil { // Quiz is already present in the store
-		return quiz, false, nil
-	}
-
-	if id != "" { // we're updating a quiz
-		quiz = s.getQuizFromID(id)
-		if quiz == nil { // Quiz is not present in the store
-			return nil, false, fmt.Errorf("Quiz ID %v doesn't exist in the store!", id)
-		}
-	} else {
-		if r.Meta != nil {
-			if r.Meta.ID != "" {
-				id = r.Meta.ID
-			} else {
-				id = uuid.New().String()
-			}
-		} else {
-			id = uuid.New().String()
-		}
-		quiz = new(models.Quiz)
-	}
-
-	if quiz.Tags == nil {
-		quiz.Tags = make([]*models.Tag, 0)
-	}
-
-	questionHash := hashes[0]
-	q := s.getQuestionFromHash(questionHash)
-	if q == nil { // if the question is not in the store then we should add it
-		q = s.createQuestionFromHash(questionHash, &models.Question{
-			Meta: models.Meta{ID: uuid.New().String()},
-			Text: s.parseTextForTags(r.Quiz.Question.Text, &quiz.Tags),
-		})
-	}
-
-	// Populate Question field
-	quiz.Question = q
-
-	// Reset answer slice
-	quiz.Answers = make([]*models.Answer, 0)
-
-	for i, answer := range r.Quiz.Answers {
-		answerHash := hashes[i+1]
-		a := s.getAnswerFromHash(answerHash)
-		if a == nil { // if the answer is not in the store add it
-			a = s.createAnswerFromHash(answerHash, &models.Answer{
-				ID:   uuid.New().String(),
-				Text: s.parseTextForTags(answer.Text, &quiz.Tags),
-			})
-		}
-		if answer.Correct {
-			quiz.Correct = a
-		}
-		quiz.Answers = append(quiz.Answers, a)
-	}
-
-	return s.createQuizFromHash(id, quizHash, quiz), true, nil
-}
-
-func (s *MemoryProboCollectorStore) getQuizFromHash(hash string) *models.Quiz {
-	s.lock.RLock()
-	defer s.lock.RUnlock()
-
-	quiz, ok := s.quizzesHashes[hash]
-	if ok {
-		return quiz
-	}
-
-	return nil
-}
-
-func (s *MemoryProboCollectorStore) getQuizFromID(id string) *models.Quiz {
-	s.lock.RLock()
-	defer s.lock.RUnlock()
-
-	quiz, ok := s.quizzes[id]
-	if ok {
-		return quiz
-	}
-
-	return nil
-}
-
-func (s *MemoryProboCollectorStore) getQuestionFromHash(hash string) *models.Question {
-	s.lock.RLock()
-	defer s.lock.RUnlock()
-
-	question, ok := s.questionsHashes[hash]
-	if ok {
-		return question
-	}
-
-	return nil
-}
-
-func (s *MemoryProboCollectorStore) getAnswerFromHash(hash string) *models.Answer {
-	s.lock.RLock()
-	defer s.lock.RUnlock()
-
-	answer, ok := s.answersHashes[hash]
-	if ok {
-		return answer
-	}
-
-	return nil
-}
-
-func (s *MemoryProboCollectorStore) createQuizFromHash(id string, hash string, quiz *models.Quiz) *models.Quiz {
-	s.lock.Lock()
-	defer s.lock.Unlock()
-
-	quiz.ID = id
-	quiz.Hash = hash
-
-	s.quizzesHashes[hash] = quiz
-	s.quizzes[id] = quiz
-
-	return quiz
-}
-
-func (s *MemoryProboCollectorStore) createQuestionFromHash(hash string, question *models.Question) *models.Question {
-	s.lock.Lock()
-	defer s.lock.Unlock()
-
-	s.questionsHashes[hash] = question
-
-	return question
-}
-
-func (s *MemoryProboCollectorStore) createAnswerFromHash(hash string, answer *models.Answer) *models.Answer {
-	s.lock.Lock()
-	defer s.lock.Unlock()
-
-	s.answersHashes[hash] = answer
-
-	return answer
-}
-
-func (s *MemoryProboCollectorStore) deleteQuiz(id string) (*models.Quiz, error) {
-	s.lock.Lock()
-	defer s.lock.Unlock()
-
-	quiz := s.quizzes[id]
-	if quiz == nil {
-		return nil, fmt.Errorf("Trying to delete a quiz that doesn't exist in memory (ID: %v)", id)
-	}
-
-	delete(s.quizzes, id)
-	delete(s.quizzesHashes, quiz.Hash)
-
-	return quiz, nil
-}

+ 28 - 1
store/participant.go

@@ -2,4 +2,31 @@ package store
 
 import "git.andreafazzi.eu/andrea/probo/models"
 
-type ParticipantStore = Store[*models.Participant]
+type ParticipantStore struct {
+	*FilterStore[*models.Participant]
+}
+
+func NewParticipantStore() *ParticipantStore {
+	store := new(ParticipantStore)
+	store.FilterStore = NewFilterStore[*models.Participant]()
+
+	return store
+}
+
+func (s *ParticipantStore) FilterInGroup(group *models.Group, filter *models.ParticipantFilter) []*models.Participant {
+	participants := s.ReadAll()
+	filteredParticipants := s.Filter(participants, func(p *models.Participant) bool {
+		for pk, pv := range p.Attributes {
+			for fk, fv := range filter.Attributes {
+				if pk == fk && pv == fv {
+					return true
+				}
+			}
+		}
+		return false
+	})
+
+	group.Participants = filteredParticipants
+
+	return group.Participants
+}