From 15830d888ffbfe5937ca7c09b02bdae747074ec0 Mon Sep 17 00:00:00 2001 From: andrea Date: Sat, 7 Oct 2023 11:43:12 +0200 Subject: [PATCH] Collections --- client/client.go | 13 ++ models/collection.go | 9 + models/meta.go | 2 +- store/file/collection_test.go | 98 ++++++++++ store/file/file.go | 257 +++++++++++++++++--------- store/file/file_test.go | 47 +++-- store/file/testdata/quizzes/quiz_5.md | 4 + store/memory/memory.go | 92 ++++++++- store/memory/memory_test.go | 59 ++++++ store/store.go | 8 +- 10 files changed, 474 insertions(+), 115 deletions(-) create mode 100644 models/collection.go create mode 100644 store/file/collection_test.go diff --git a/client/client.go b/client/client.go index d06cd14..acdafdb 100644 --- a/client/client.go +++ b/client/client.go @@ -16,6 +16,11 @@ type Quiz struct { Answers []*Answer `json:"answers"` } +type Collection struct { + Name string `json:"name"` + Query string `json:"query"` +} + type BaseResponse struct { Status string `json:"status"` Message string `json:"message"` @@ -52,3 +57,11 @@ type CreateUpdateQuizRequest struct { type DeleteQuizRequest struct { ID string } + +type CreateUpdateCollectionRequest struct { + *Collection +} + +type DeleteCollectionRequest struct { + ID string +} diff --git a/models/collection.go b/models/collection.go new file mode 100644 index 0000000..e33f9c8 --- /dev/null +++ b/models/collection.go @@ -0,0 +1,9 @@ +package models + +type Collection struct { + ID string `json:"id"` + Name string `json:"name"` + Query string `json:"query"` + + IDs []string `json:"ids"` +} diff --git a/models/meta.go b/models/meta.go index aa69ad1..0ba0790 100644 --- a/models/meta.go +++ b/models/meta.go @@ -5,5 +5,5 @@ import "time" type Meta struct { ID string `json:"id" yaml:"id"` CreatedAt time.Time `json:"created_at" yaml:"created_at"` - Tags []*Tag `json:"tags" yaml:"tags"` + Tags []*Tag `json:"tags" yaml:"-"` } diff --git a/store/file/collection_test.go b/store/file/collection_test.go new file mode 100644 index 0000000..472e522 --- /dev/null +++ b/store/file/collection_test.go @@ -0,0 +1,98 @@ +package file + +import ( + "fmt" + "os" + + "git.andreafazzi.eu/andrea/probo/client" + "github.com/remogatto/prettytest" +) + +type collectionTestSuite struct { + prettytest.Suite +} + +func (t *collectionTestSuite) TestCreateCollection() { + store, err := NewFileProboCollectorStore(testdataDir) + t.Nil(err, fmt.Sprintf("A file store should be initialized without problems but an error occurred: %v", err)) + + if !t.Failed() { + quiz_1, err := createQuizOnDisk(store, &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}, + }, + }, + }) + + t.Nil(err, "The quiz to be updated should be created without issue") + + path_1, _ := store.GetQuizPath(quiz_1) + + if !t.Failed() { + quiz_2, err := createQuizOnDisk(store, &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}, + }, + }, + }) + + t.Nil(err, "The quiz to be updated should be created without issue") + + path_2, _ := store.GetQuizPath(quiz_2) + + if !t.Failed() { + + quiz_3, err := createQuizOnDisk(store, &client.CreateUpdateQuizRequest{ + Quiz: &client.Quiz{ + Question: &client.Question{Text: "Question text without tags."}, + Answers: []*client.Answer{ + {Text: "Answer 1", Correct: true}, + {Text: "Answer 2", Correct: false}, + {Text: "Answer 3", Correct: false}, + {Text: "Answer 4", Correct: false}, + }, + }, + }) + + t.Nil(err, "The quiz to be updated should be created without issue") + + path_3, _ := store.GetQuizPath(quiz_3) + + if !t.Failed() { + + collection, err := store.CreateCollection( + &client.CreateUpdateCollectionRequest{ + Collection: &client.Collection{ + Name: "MyCollection", + Query: "#tag1", + }, + }) + + t.Nil(err, "Creating a collection should not return an error") + + collectionPath, _ := store.GetCollectionPath(collection) + + if !t.Failed() { + t.Equal(2, len(collection.IDs)) + + os.Remove(path_1) + os.Remove(path_2) + os.Remove(path_3) + os.Remove(collectionPath) + + } + } + } + } + } +} diff --git a/store/file/file.go b/store/file/file.go index edf2c36..28a45b4 100644 --- a/store/file/file.go +++ b/store/file/file.go @@ -3,6 +3,7 @@ package file import ( "bufio" "bytes" + "encoding/json" "errors" "fmt" "io" @@ -22,13 +23,23 @@ import ( "github.com/go-yaml/yaml" ) -var ErrorMetaHeaderIsNotPresent = errors.New("Meta header was not found in file.") +var ( + ErrorMetaHeaderIsNotPresent = errors.New("Meta header was not found in file.") + + DefaultQuizzesDir = "quizzes" + DefaultCollectionsDir = "collections" +) type FileProboCollectorStore struct { Dir string memoryStore *memory.MemoryProboCollectorStore - paths map[string]string + + quizzesPaths map[string]string + collectionsPaths map[string]string + + quizzesDir string + collectionsDir string // A mutex is used to synchronize read/write access to the map lock sync.RWMutex @@ -39,6 +50,9 @@ func NewFileProboCollectorStore(dirname string) (*FileProboCollectorStore, error s.Dir = dirname + s.quizzesDir = filepath.Join(s.Dir, DefaultQuizzesDir) + s.collectionsDir = filepath.Join(s.Dir, DefaultCollectionsDir) + err := s.Reindex() if err != nil { return nil, err @@ -47,14 +61,20 @@ func NewFileProboCollectorStore(dirname string) (*FileProboCollectorStore, error return s, nil } -func (s *FileProboCollectorStore) Reindex() error { - files, err := ioutil.ReadDir(s.Dir) +func (s *FileProboCollectorStore) GetQuizzesDir() string { + return s.quizzesDir +} + +func (s *FileProboCollectorStore) GetCollectionsDir() string { + return s.collectionsDir +} + +func (s *FileProboCollectorStore) reindexQuizzes() error { + files, err := ioutil.ReadDir(s.quizzesDir) if err != nil { return err } - s.paths = make(map[string]string) - markdownFiles := make([]fs.FileInfo, 0) for _, file := range files { @@ -68,13 +88,9 @@ func (s *FileProboCollectorStore) Reindex() error { return fmt.Errorf("The directory is empty.") } - s.memoryStore = memory.NewMemoryProboCollectorStore( - sha256.NewDefault256Hasher(sha256.DefaultSHA256HashingFn), - ) - for _, file := range markdownFiles { filename := file.Name() - fullPath := filepath.Join(s.Dir, filename) + fullPath := filepath.Join(s.quizzesDir, filename) content, err := os.ReadFile(fullPath) if err != nil { @@ -98,7 +114,72 @@ func (s *FileProboCollectorStore) Reindex() error { CreatedAt: time.Now(), }) } - s.SetPath(q, fullPath) + s.SetQuizPath(q, fullPath) + } + + return nil +} + +func (s *FileProboCollectorStore) reindexCollections() error { + files, err := ioutil.ReadDir(s.collectionsDir) + if err != nil { + return err + } + + jsonFiles := make([]fs.FileInfo, 0) + + for _, file := range files { + filename := file.Name() + if !file.IsDir() && strings.HasSuffix(filename, ".json") { + jsonFiles = append(jsonFiles, file) + } + } + + for _, file := range jsonFiles { + filename := file.Name() + fullPath := filepath.Join(s.collectionsDir, filename) + + content, err := os.ReadFile(fullPath) + if err != nil { + return err + } + + var clientCollection *client.Collection + + err = json.Unmarshal(content, &clientCollection) + if err != nil { + return err + } + + collection, err := s.memoryStore.CreateCollection(&client.CreateUpdateCollectionRequest{ + Collection: clientCollection, + }) + if err != nil { + return err + } + + s.SetCollectionPath(collection, fullPath) + } + + return nil +} + +func (s *FileProboCollectorStore) Reindex() error { + s.memoryStore = memory.NewMemoryProboCollectorStore( + sha256.NewDefault256Hasher(sha256.DefaultSHA256HashingFn), + ) + + s.quizzesPaths = make(map[string]string) + s.collectionsPaths = make(map[string]string) + + err := s.reindexQuizzes() + if err != nil { + return err + } + + err = s.reindexCollections() + if err != nil { + return err } return nil @@ -153,7 +234,7 @@ func (s *FileProboCollectorStore) DeleteQuiz(r *client.DeleteQuizRequest) (*mode return nil, err } - path, err := s.GetPath(quiz) + path, err := s.GetQuizPath(quiz) if err != nil { return nil, err } @@ -171,21 +252,47 @@ func (s *FileProboCollectorStore) DeleteQuiz(r *client.DeleteQuizRequest) (*mode return quiz, nil } -func (s *FileProboCollectorStore) GetPath(quiz *models.Quiz) (string, error) { +func (s *FileProboCollectorStore) GetQuizPath(quiz *models.Quiz) (string, error) { + if quiz == nil { + return "", errors.New("Quiz object passed as argument is nil!") + } + s.lock.RLock() defer s.lock.RUnlock() - path, ok := s.paths[quiz.ID] + path, ok := s.quizzesPaths[quiz.ID] if !ok { return "", errors.New(fmt.Sprintf("Path not found for quiz ID %v", quiz.ID)) } return path, nil } -func (s *FileProboCollectorStore) SetPath(quiz *models.Quiz, path string) string { +func (s *FileProboCollectorStore) GetCollectionPath(collection *models.Collection) (string, error) { + s.lock.RLock() + defer s.lock.RUnlock() + + path, ok := s.collectionsPaths[collection.ID] + if !ok { + return "", errors.New(fmt.Sprintf("Path not found for collection ID %v", collection.ID)) + } + return path, nil +} + +func (s *FileProboCollectorStore) SetQuizPath(quiz *models.Quiz, path string) string { s.lock.Lock() defer s.lock.Unlock() - s.paths[quiz.ID] = path + + s.quizzesPaths[quiz.ID] = path + + return path +} + +func (s *FileProboCollectorStore) SetCollectionPath(collection *models.Collection, path string) string { + s.lock.Lock() + defer s.lock.Unlock() + + s.collectionsPaths[collection.ID] = path + return path } @@ -216,67 +323,6 @@ func MarkdownFromQuiz(quiz *models.Quiz) (string, error) { return markdown, nil } -// func QuizFromMarkdown(markdown string) (*client.Quiz, *models.Meta, error) { -// meta, remainingMarkdown, err := parseMetaHeaderFromMarkdown(markdown) -// if err != nil { -// return nil, nil, err -// } - -// if meta == nil { -// meta = new(models.Meta) -// if meta.Tags == nil { -// meta.Tags = make([]*models.Tag, 0) -// } -// } else if meta.Tags == nil { -// meta.Tags = make([]*models.Tag, 0) -// } - -// lines := strings.Split(remainingMarkdown, "\n") - -// questionText := "" -// answers := []*client.Answer{} - -// for _, line := range lines { -// // Check if the line contains a tag -// if strings.Contains(line, "#") { -// // Split the line into words -// words := strings.Split(line, " ") -// for _, word := range words { -// // If the word starts with '#', add it to the tags -// if strings.HasPrefix(word, "#") { -// meta.Tags = append(meta.Tags, &models.Tag{Name: word[1:]}) -// } -// } -// } - -// if strings.HasPrefix(line, "*") { -// answerText := strings.TrimPrefix(line, "* ") -// correct := len(answers) == 0 -// answer := &client.Answer{Text: answerText, Correct: correct} -// answers = append(answers, answer) -// } else { -// if questionText != "" { -// questionText += "\n" -// } -// questionText += line -// } -// } - -// questionText = strings.TrimRight(questionText, "\n") - -// if questionText == "" { -// return nil, nil, fmt.Errorf("Question text should not be empty.") -// } - -// if len(answers) < 2 { -// return nil, nil, fmt.Errorf("Number of answers should be at least 2 but parsed answers are %d.", len(answers)) -// } -// question := &client.Question{Text: questionText} -// quiz := &client.Quiz{Question: question, Answers: answers} - -// return quiz, meta, nil -// } - func QuizFromMarkdown(markdown string) (*client.Quiz, *models.Meta, error) { meta, remainingMarkdown, err := parseMetaHeaderFromMarkdown(markdown) if err != nil { @@ -319,7 +365,7 @@ func QuizFromMarkdown(markdown string) (*client.Quiz, *models.Meta, error) { } func (s *FileProboCollectorStore) ReadMetaHeaderFromFile(filename string) (*models.Meta, error) { - data, err := ioutil.ReadFile(path.Join(s.Dir, filename)) + data, err := ioutil.ReadFile(path.Join(s.quizzesDir, filename)) if err != nil { return nil, err } @@ -336,7 +382,7 @@ func (s *FileProboCollectorStore) WriteMetaHeaderToFile(filename string, meta *m return nil, err } if readMeta == nil { - _, err := writeMetaHeader(path.Join(s.Dir, filename), meta) + _, err := writeMetaHeader(path.Join(s.quizzesDir, filename), meta) if err != nil { return nil, err } @@ -345,8 +391,22 @@ func (s *FileProboCollectorStore) WriteMetaHeaderToFile(filename string, meta *m return meta, nil } +func (s *FileProboCollectorStore) CreateCollection(r *client.CreateUpdateCollectionRequest) (*models.Collection, error) { + collection, err := s.memoryStore.CreateCollection(r) + if err != nil { + return nil, err + } + + err = s.createOrUpdateCollectionFile(collection) + if err != nil { + return nil, err + } + + return s.memoryStore.ReadCollectionByID(collection.ID) +} + func (s *FileProboCollectorStore) removeMetaFromFile(filename string) (*models.Meta, error) { - file, err := os.Open(path.Join(s.Dir, filename)) + file, err := os.Open(path.Join(s.quizzesDir, filename)) if err != nil { return nil, err } @@ -399,7 +459,7 @@ func (s *FileProboCollectorStore) removeMetaFromFile(filename string) (*models.M return nil, err } - file, err = os.Create(path.Join(s.Dir, filename)) + file, err = os.Create(path.Join(s.quizzesDir, filename)) if err != nil { return nil, err } @@ -419,15 +479,16 @@ func (s *FileProboCollectorStore) createOrUpdateMarkdownFile(quiz *models.Quiz) return err } - fn, _ := s.GetPath(quiz) + fn, _ := s.GetQuizPath(quiz) if fn == "" { - fn = filepath.Join(s.Dir, fmt.Sprintf("quiz_%v.%s", time.Now().Unix(), "md")) + fn = filepath.Join(s.quizzesDir, fmt.Sprintf("quiz_%v.%s", quiz.ID, "md")) } file, err := os.Create(fn) if err != nil { return err } + defer file.Close() markdownWithMetaHeader, err := addMetaHeaderToMarkdown(markdown, &quiz.Meta) @@ -440,11 +501,37 @@ func (s *FileProboCollectorStore) createOrUpdateMarkdownFile(quiz *models.Quiz) return err } - s.SetPath(quiz, fn) + s.SetQuizPath(quiz, fn) return nil } +func (s *FileProboCollectorStore) createOrUpdateCollectionFile(collection *models.Collection) error { + json, err := json.Marshal(collection) + if err != nil { + return err + } + + fn, _ := s.GetCollectionPath(collection) + if fn == "" { + fn = filepath.Join(s.collectionsDir, fmt.Sprintf("collection_%v.%s", collection.ID, "json")) + } + + file, err := os.Create(fn) + if err != nil { + return err + } + defer file.Close() + + _, err = file.Write([]byte(json)) + if err != nil { + return err + } + + s.SetCollectionPath(collection, fn) + + return nil +} func addMetaHeaderToMarkdown(content string, meta *models.Meta) (string, error) { var buffer bytes.Buffer diff --git a/store/file/file_test.go b/store/file/file_test.go index 3a0d70f..7f23a7e 100644 --- a/store/file/file_test.go +++ b/store/file/file_test.go @@ -13,18 +13,21 @@ import ( "github.com/remogatto/prettytest" ) -type testSuite struct { +var testdataDir = "./testdata" + +type quizTestSuite struct { prettytest.Suite } func TestRunner(t *testing.T) { prettytest.Run( t, - new(testSuite), + new(quizTestSuite), + new(collectionTestSuite), ) } -func (t *testSuite) TestQuizFromMarkdown() { +func (t *quizTestSuite) TestQuizFromMarkdown() { markdown := `Question text (1). Question text (2). @@ -55,8 +58,8 @@ Question text with #tag1 #tag2 (3). } } -func (t *testSuite) TestReadAllQuizzes() { - store, err := NewFileProboCollectorStore("./testdata/quizzes") +func (t *quizTestSuite) TestReadAllQuizzes() { + store, err := NewFileProboCollectorStore("./testdata/") t.True(err == nil, fmt.Sprintf("A file store should be initialized without problems but an error occurred: %v", err)) if !t.Failed() { @@ -75,7 +78,7 @@ func (t *testSuite) TestReadAllQuizzes() { } -func (t *testSuite) TestMarkdownFromQuiz() { +func (t *quizTestSuite) TestMarkdownFromQuiz() { store := memory.NewMemoryProboCollectorStore(sha256.NewDefault256Hasher(sha256.DefaultSHA256HashingFn)) quiz, err := store.CreateQuiz( &client.CreateUpdateQuizRequest{ @@ -102,9 +105,8 @@ func (t *testSuite) TestMarkdownFromQuiz() { } } -func (t *testSuite) TestCreateQuiz() { - dirname := "./testdata/quizzes" - store, err := NewFileProboCollectorStore(dirname) +func (t *quizTestSuite) TestCreateQuiz() { + store, err := NewFileProboCollectorStore(testdataDir) t.True(err == nil, fmt.Sprintf("A file store should be initialized without problems but an error occurred: %v", err)) @@ -127,7 +129,7 @@ func (t *testSuite) TestCreateQuiz() { t.Nil(err, fmt.Sprintf("An error was raised when saving the quiz on disk: %v", err)) if !t.Failed() { - path, err := store.GetPath(quiz) + path, err := store.GetQuizPath(quiz) t.Nil(err, "GetPath should not raise an error.") if !t.Failed() { @@ -151,9 +153,8 @@ func (t *testSuite) TestCreateQuiz() { } } -func (t *testSuite) TestDeleteQuiz() { - dirname := "./testdata/quizzes" - store, err := NewFileProboCollectorStore(dirname) +func (t *quizTestSuite) TestDeleteQuiz() { + store, err := NewFileProboCollectorStore(testdataDir) t.True(err == nil, fmt.Sprintf("A file store should be initialized without problems but an error occurred: %v", err)) if !t.Failed() { @@ -171,7 +172,7 @@ func (t *testSuite) TestDeleteQuiz() { t.Nil(err, "The quiz to be deleted should be created without issue") - path, err := store.GetPath(quiz) + path, err := store.GetQuizPath(quiz) t.True(path != "", "Quiz path should be obtained without errors") if !t.Failed() { @@ -184,9 +185,8 @@ func (t *testSuite) TestDeleteQuiz() { } } -func (t *testSuite) TestUpdateQuiz() { - dirname := "./testdata/quizzes" - store, err := NewFileProboCollectorStore(dirname) +func (t *quizTestSuite) TestUpdateQuiz() { + store, err := NewFileProboCollectorStore(testdataDir) t.True(err == nil, fmt.Sprintf("A file store should be initialized without problems but an error occurred: %v", err)) if !t.Failed() { @@ -225,7 +225,7 @@ func (t *testSuite) TestUpdateQuiz() { t.True(len(updatedQuiz.Tags) == 1, "Length of tags array should be 1") if !t.Failed() { - path, err := store.GetPath(updatedQuiz) + path, err := store.GetQuizPath(updatedQuiz) if !t.Failed() { t.Nil(err, "GetPath should not raise an error.") @@ -246,11 +246,9 @@ func (t *testSuite) TestUpdateQuiz() { } -func (t *testSuite) TestReadMetaHeaderFromFile() { - dirname := "./testdata/quizzes" - store, err := NewFileProboCollectorStore(dirname) +func (t *quizTestSuite) TestReadMetaHeaderFromFile() { + store, err := NewFileProboCollectorStore(testdataDir) t.True(err == nil, fmt.Sprintf("A file store should be initialized without problems but an error occurred: %v", err)) - meta, err := store.ReadMetaHeaderFromFile("quiz_4.md") t.True(err == nil, fmt.Sprintf("An error occurred: %v", err)) if !t.Failed() { @@ -259,9 +257,8 @@ func (t *testSuite) TestReadMetaHeaderFromFile() { } } -func (t *testSuite) TestWriteMetaHeaderToFile() { - dirname := "./testdata/quizzes" - store, err := NewFileProboCollectorStore(dirname) +func (t *quizTestSuite) TestWriteMetaHeaderToFile() { + store, err := NewFileProboCollectorStore(testdataDir) t.True(err == nil, fmt.Sprintf("A file store should be initialized without problems but an error occurred: %v", err)) diff --git a/store/file/testdata/quizzes/quiz_5.md b/store/file/testdata/quizzes/quiz_5.md index 6331e1a..0c1e47a 100644 --- a/store/file/testdata/quizzes/quiz_5.md +++ b/store/file/testdata/quizzes/quiz_5.md @@ -1,3 +1,7 @@ +--- +id: f7034ebc-d62d-43eb-a6cf-57f4886c3a7c +created_at: !!timestamp 2023-10-07T11:38:32.049804383+02:00 +--- This quiz is initially without metadata. * Answer 1 diff --git a/store/memory/memory.go b/store/memory/memory.go index f1536f7..599246b 100644 --- a/store/memory/memory.go +++ b/store/memory/memory.go @@ -1,6 +1,7 @@ package memory import ( + "errors" "fmt" "strings" "sync" @@ -12,8 +13,8 @@ import ( ) type MemoryProboCollectorStore struct { - quizzes map[string]*models.Quiz - + quizzes map[string]*models.Quiz + collections map[string]*models.Collection questionsHashes map[string]*models.Question answersHashes map[string]*models.Answer quizzesHashes map[string]*models.Quiz @@ -34,6 +35,7 @@ func NewMemoryProboCollectorStore(hasher hasher.Hasher) *MemoryProboCollectorSto s.quizzesHashes = make(map[string]*models.Quiz) s.quizzes = make(map[string]*models.Quiz) + s.collections = make(map[string]*models.Collection) return s } @@ -62,6 +64,18 @@ func (s *MemoryProboCollectorStore) getQuizFromID(id string) *models.Quiz { return nil } +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) getQuestionFromHash(hash string) *models.Question { s.lock.RLock() defer s.lock.RUnlock() @@ -185,6 +199,9 @@ func (s *MemoryProboCollectorStore) parseTextForTags(text string, tags *[]*model } 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] @@ -260,3 +277,74 @@ func (s *MemoryProboCollectorStore) UpdateQuiz(r *client.CreateUpdateQuizRequest func (s *MemoryProboCollectorStore) DeleteQuiz(id string) (*models.Quiz, error) { return s.deleteQuiz(id) } + +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) { + collection := s.getCollectionFromID(id) + if collection == nil { + return nil, fmt.Errorf("Collection ID %v not found in the store", collection.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.IDs = s.query(collection.Query) + + return s.createCollectionFromID(id, collection), true, nil +} + +func (s *MemoryProboCollectorStore) query(query string) []string { + s.lock.Lock() + defer s.lock.Unlock() + + result := make([]string, 0) + + for id, quiz := range s.quizzes { + for _, tag := range quiz.Tags { + if query == tag.Name { + result = append(result, id) + 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/memory_test.go b/store/memory/memory_test.go index ef54564..35b3e19 100644 --- a/store/memory/memory_test.go +++ b/store/memory/memory_test.go @@ -139,3 +139,62 @@ func (t *testSuite) TestDeleteQuiz() { _, err = store.ReadQuizByHash(quiz.Hash) t.True(err != nil, "Reading a non existent quiz should return an error") } + +func (t *testSuite) 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.IDs) == 2) + if !t.Failed() { + t.Equal(quiz_1.ID, updatedCollection.IDs[0]) + t.Equal(quiz_2.ID, updatedCollection.IDs[1]) + } + } +} diff --git a/store/store.go b/store/store.go index a8b7985..2a621eb 100644 --- a/store/store.go +++ b/store/store.go @@ -7,10 +7,14 @@ import ( type ProboCollectorStore interface { ReadAllQuizzes() ([]*models.Quiz, error) - ReadQuizByHash(hash string) (*models.Quiz, error) - CreateQuiz(r *client.CreateUpdateQuizRequest) (*models.Quiz, error) UpdateQuiz(r *client.CreateUpdateQuizRequest, id string) (*models.Quiz, error) DeleteQuiz(r *client.DeleteQuizRequest) (*models.Quiz, error) + + ReadAllCollections() ([]*models.Collection, error) + ReadCollectionByID(id string) (*models.Collection, error) + CreateCollection(r *client.CreateUpdateCollectionRequest) (*models.Collection, error) + UpdateCollection(r *client.CreateUpdateCollectionRequest, id string) (*models.Collection, error) + DeleteCollection(r *client.DeleteCollectionRequest) (*models.Collection, error) }