diff --git a/models/collection.go b/models/collection.go index ce230cd..d9593e5 100644 --- a/models/collection.go +++ b/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{} +} diff --git a/models/filters.go b/models/filters.go new file mode 100644 index 0000000..cda0b63 --- /dev/null +++ b/models/filters.go @@ -0,0 +1,9 @@ +package models + +type Filter struct { + Tags []*Tag +} + +type ParticipantFilter struct { + Attributes map[string]string +} diff --git a/models/group.go b/models/group.go index ac6a1b1..c48beb2 100644 --- a/models/group.go +++ b/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) +} diff --git a/models/group_test.go b/models/group_test.go new file mode 100644 index 0000000..932dc1c --- /dev/null +++ b/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)) +} diff --git a/models/models_test.go b/models/models_test.go index b7727dd..7a6fa1b 100644 --- a/models/models_test.go +++ b/models/models_test.go @@ -16,6 +16,7 @@ func TestRunner(t *testing.T) { prettytest.Run( t, new(testSuite), + new(groupTestSuite), ) } diff --git a/models/participant.go b/models/participant.go index 380556a..b0e5570 100644 --- a/models/participant.go +++ b/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(), ",") +} diff --git a/store/file/collection.go b/store/file/collection.go index 3f7eb3b..6250e7b 100644 --- a/store/file/collection.go +++ b/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], }, ) diff --git a/store/file/defaults.go b/store/file/defaults.go index 9bc076f..4646515 100644 --- a/store/file/defaults.go +++ b/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) +} diff --git a/store/file/file.go b/store/file/file.go index ba26351..d854ce8 100644 --- a/store/file/file.go +++ b/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 { diff --git a/store/file/file_test.go b/store/file/file_test.go index 7c779a2..3169eae 100644 --- a/store/file/file_test.go +++ b/store/file/file_test.go @@ -16,5 +16,6 @@ func TestRunner(t *testing.T) { new(quizTestSuite), new(collectionTestSuite), new(participantTestSuite), + new(groupTestSuite), ) } diff --git a/store/file/group.go b/store/file/group.go new file mode 100644 index 0000000..9fc71f4 --- /dev/null +++ b/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], + }, + ) + +} diff --git a/store/file/group_test.go b/store/file/group_test.go new file mode 100644 index 0000000..72ee304 --- /dev/null +++ b/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 +} diff --git a/store/file/participant.go b/store/file/participant.go index ced923d..3185b9f 100644 --- a/store/file/participant.go +++ b/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], + }, + ) } diff --git a/store/file/quiz.go b/store/file/quiz.go index 5f2247c..04beeb2 100644 --- a/store/file/quiz.go +++ b/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 } diff --git a/store/file/quiz_test.go b/store/file/quiz_test.go index f4f7898..a46a3e9 100644 --- a/store/file/quiz_test.go +++ b/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) +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() { + 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.") + if !t.Failed() { + path := store.GetPath(quiz) + t.True(path != "", "Path should not be empty.") -// exists, err := os.Stat(path) -// t.Nil(err) + 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() { + t.True(exists != nil, "The new quiz file was not created.") -// if !t.Failed() { -// quizFromDisk, err := readQuizFromDisk(path) -// defer os.Remove(path) + if !t.Failed() { + quizFromDisk, err := readQuizFromDisk(path) + defer os.Remove(path) -// quizFromDisk.Correct = quiz.Answers[0] -// quizFromDisk.Tags = quiz.Tags + quizFromDisk.Correct = quiz.Answers[0] + quizFromDisk.Tags = quiz.Tags -// t.Nil(err) + 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) -// } -// } -// } -// } + 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) +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)) + 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) +func (t *quizTestSuite) TestUpdate() { + store, err := NewQuizFileStore( + &FileStoreConfig[*models.Quiz, *store.QuizStore]{ + FilePathConfig: FilePathConfig{GetDefaultQuizzesDir(), "quiz", ".md"}, + IndexDirFunc: DefaultQuizIndexDirFunc, + NoIndexOnCreate: true, + }, + ) -// 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.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) + 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.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) -// updatedQuizFromMemory, err := store.Read(quiz.ID) -// t.Equal(len(updatedQuizFromMemory.Tags), 3) -// t.Equal("Answer 2", updatedQuizFromMemory.Correct.Text) + t.Nil(err) -// defer os.Remove(store.GetPath(quiz)) + 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() { +func (t *quizTestSuite) TestAutowriteHeader() { + store, err := NewDefaultQuizFileStore() + t.Nil(err) -// meta, err := readQuizHeader(filepath.Join(store.Dir, "quiz_5.md")) -// 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.Not(t.Nil(meta)) + if !t.Failed() { + t.True(meta.ID != "", "ID should not be empty") -// 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) + } + } + } + } +} -// 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 + } -// func readQuizFromDisk(path string) (*models.Quiz, error) { -// content, err := os.ReadFile(path) -// if err != nil { -// return nil, err -// } + result := new(models.Quiz) -// result := new(models.Quiz) + err = result.Unmarshal(content) + if err != nil { + return nil, err + } -// err = result.Unmarshal(content) -// if err != nil { -// return nil, err -// } - -// return result, nil -// } + return result, nil +} diff --git a/store/file/testdata/groups/group_96a300bb-0b29-4b32-93cf-5bcde7fcef61.csv b/store/file/testdata/groups/group_96a300bb-0b29-4b32-93cf-5bcde7fcef61.csv new file mode 100644 index 0000000..564e88d --- /dev/null +++ b/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 diff --git a/store/group.go b/store/group.go new file mode 100644 index 0000000..5b314c3 --- /dev/null +++ b/store/group.go @@ -0,0 +1,5 @@ +package store + +import "git.andreafazzi.eu/andrea/probo/models" + +type GroupStore = Store[*models.Group] diff --git a/store/memory/collection.go b/store/memory/collection.go deleted file mode 100644 index 9bf2a29..0000000 --- a/store/memory/collection.go +++ /dev/null @@ -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 -} diff --git a/store/memory/collection_test.go b/store/memory/collection_test.go deleted file mode 100644 index dc2c7a5..0000000 --- a/store/memory/collection_test.go +++ /dev/null @@ -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") -} diff --git a/store/memory/memory.go b/store/memory/memory.go deleted file mode 100644 index e036e38..0000000 --- a/store/memory/memory.go +++ /dev/null @@ -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 -} diff --git a/store/memory/memory_test.go b/store/memory/memory_test.go deleted file mode 100644 index 5ec27ae..0000000 --- a/store/memory/memory_test.go +++ /dev/null @@ -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") -} diff --git a/store/memory/participant.go b/store/memory/participant.go deleted file mode 100644 index 12ec80b..0000000 --- a/store/memory/participant.go +++ /dev/null @@ -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 -} diff --git a/store/memory/participant_test.go b/store/memory/participant_test.go deleted file mode 100644 index 2b460d8..0000000 --- a/store/memory/participant_test.go +++ /dev/null @@ -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") -} diff --git a/store/memory/quiz.go b/store/memory/quiz.go deleted file mode 100644 index 36e9635..0000000 --- a/store/memory/quiz.go +++ /dev/null @@ -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 -} diff --git a/store/participant.go b/store/participant.go index 60fb66c..ae7e702 100644 --- a/store/participant.go +++ b/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 +}