Collections

This commit is contained in:
andrea 2023-10-07 11:43:12 +02:00
parent 4da6162dd4
commit 15830d888f
10 changed files with 474 additions and 115 deletions

View file

@ -16,6 +16,11 @@ type Quiz struct {
Answers []*Answer `json:"answers"` Answers []*Answer `json:"answers"`
} }
type Collection struct {
Name string `json:"name"`
Query string `json:"query"`
}
type BaseResponse struct { type BaseResponse struct {
Status string `json:"status"` Status string `json:"status"`
Message string `json:"message"` Message string `json:"message"`
@ -52,3 +57,11 @@ type CreateUpdateQuizRequest struct {
type DeleteQuizRequest struct { type DeleteQuizRequest struct {
ID string ID string
} }
type CreateUpdateCollectionRequest struct {
*Collection
}
type DeleteCollectionRequest struct {
ID string
}

9
models/collection.go Normal file
View file

@ -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"`
}

View file

@ -5,5 +5,5 @@ import "time"
type Meta struct { type Meta struct {
ID string `json:"id" yaml:"id"` ID string `json:"id" yaml:"id"`
CreatedAt time.Time `json:"created_at" yaml:"created_at"` CreatedAt time.Time `json:"created_at" yaml:"created_at"`
Tags []*Tag `json:"tags" yaml:"tags"` Tags []*Tag `json:"tags" yaml:"-"`
} }

View file

@ -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)
}
}
}
}
}
}

View file

@ -3,6 +3,7 @@ package file
import ( import (
"bufio" "bufio"
"bytes" "bytes"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"io" "io"
@ -22,13 +23,23 @@ import (
"github.com/go-yaml/yaml" "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 { type FileProboCollectorStore struct {
Dir string Dir string
memoryStore *memory.MemoryProboCollectorStore 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 // A mutex is used to synchronize read/write access to the map
lock sync.RWMutex lock sync.RWMutex
@ -39,6 +50,9 @@ func NewFileProboCollectorStore(dirname string) (*FileProboCollectorStore, error
s.Dir = dirname s.Dir = dirname
s.quizzesDir = filepath.Join(s.Dir, DefaultQuizzesDir)
s.collectionsDir = filepath.Join(s.Dir, DefaultCollectionsDir)
err := s.Reindex() err := s.Reindex()
if err != nil { if err != nil {
return nil, err return nil, err
@ -47,14 +61,20 @@ func NewFileProboCollectorStore(dirname string) (*FileProboCollectorStore, error
return s, nil return s, nil
} }
func (s *FileProboCollectorStore) Reindex() error { func (s *FileProboCollectorStore) GetQuizzesDir() string {
files, err := ioutil.ReadDir(s.Dir) 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 { if err != nil {
return err return err
} }
s.paths = make(map[string]string)
markdownFiles := make([]fs.FileInfo, 0) markdownFiles := make([]fs.FileInfo, 0)
for _, file := range files { for _, file := range files {
@ -68,13 +88,9 @@ func (s *FileProboCollectorStore) Reindex() error {
return fmt.Errorf("The directory is empty.") return fmt.Errorf("The directory is empty.")
} }
s.memoryStore = memory.NewMemoryProboCollectorStore(
sha256.NewDefault256Hasher(sha256.DefaultSHA256HashingFn),
)
for _, file := range markdownFiles { for _, file := range markdownFiles {
filename := file.Name() filename := file.Name()
fullPath := filepath.Join(s.Dir, filename) fullPath := filepath.Join(s.quizzesDir, filename)
content, err := os.ReadFile(fullPath) content, err := os.ReadFile(fullPath)
if err != nil { if err != nil {
@ -98,7 +114,72 @@ func (s *FileProboCollectorStore) Reindex() error {
CreatedAt: time.Now(), 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 return nil
@ -153,7 +234,7 @@ func (s *FileProboCollectorStore) DeleteQuiz(r *client.DeleteQuizRequest) (*mode
return nil, err return nil, err
} }
path, err := s.GetPath(quiz) path, err := s.GetQuizPath(quiz)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -171,21 +252,47 @@ func (s *FileProboCollectorStore) DeleteQuiz(r *client.DeleteQuizRequest) (*mode
return quiz, nil 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() s.lock.RLock()
defer s.lock.RUnlock() defer s.lock.RUnlock()
path, ok := s.paths[quiz.ID] path, ok := s.quizzesPaths[quiz.ID]
if !ok { if !ok {
return "", errors.New(fmt.Sprintf("Path not found for quiz ID %v", quiz.ID)) return "", errors.New(fmt.Sprintf("Path not found for quiz ID %v", quiz.ID))
} }
return path, nil 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() s.lock.Lock()
defer s.lock.Unlock() 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 return path
} }
@ -216,67 +323,6 @@ func MarkdownFromQuiz(quiz *models.Quiz) (string, error) {
return markdown, nil 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) { func QuizFromMarkdown(markdown string) (*client.Quiz, *models.Meta, error) {
meta, remainingMarkdown, err := parseMetaHeaderFromMarkdown(markdown) meta, remainingMarkdown, err := parseMetaHeaderFromMarkdown(markdown)
if err != nil { 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) { 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 { if err != nil {
return nil, err return nil, err
} }
@ -336,7 +382,7 @@ func (s *FileProboCollectorStore) WriteMetaHeaderToFile(filename string, meta *m
return nil, err return nil, err
} }
if readMeta == nil { if readMeta == nil {
_, err := writeMetaHeader(path.Join(s.Dir, filename), meta) _, err := writeMetaHeader(path.Join(s.quizzesDir, filename), meta)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -345,8 +391,22 @@ func (s *FileProboCollectorStore) WriteMetaHeaderToFile(filename string, meta *m
return meta, nil 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) { 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 { if err != nil {
return nil, err return nil, err
} }
@ -399,7 +459,7 @@ func (s *FileProboCollectorStore) removeMetaFromFile(filename string) (*models.M
return nil, err return nil, err
} }
file, err = os.Create(path.Join(s.Dir, filename)) file, err = os.Create(path.Join(s.quizzesDir, filename))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -419,15 +479,16 @@ func (s *FileProboCollectorStore) createOrUpdateMarkdownFile(quiz *models.Quiz)
return err return err
} }
fn, _ := s.GetPath(quiz) fn, _ := s.GetQuizPath(quiz)
if fn == "" { 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) file, err := os.Create(fn)
if err != nil { if err != nil {
return err return err
} }
defer file.Close() defer file.Close()
markdownWithMetaHeader, err := addMetaHeaderToMarkdown(markdown, &quiz.Meta) markdownWithMetaHeader, err := addMetaHeaderToMarkdown(markdown, &quiz.Meta)
@ -440,11 +501,37 @@ func (s *FileProboCollectorStore) createOrUpdateMarkdownFile(quiz *models.Quiz)
return err return err
} }
s.SetPath(quiz, fn) s.SetQuizPath(quiz, fn)
return nil 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) { func addMetaHeaderToMarkdown(content string, meta *models.Meta) (string, error) {
var buffer bytes.Buffer var buffer bytes.Buffer

View file

@ -13,18 +13,21 @@ import (
"github.com/remogatto/prettytest" "github.com/remogatto/prettytest"
) )
type testSuite struct { var testdataDir = "./testdata"
type quizTestSuite struct {
prettytest.Suite prettytest.Suite
} }
func TestRunner(t *testing.T) { func TestRunner(t *testing.T) {
prettytest.Run( prettytest.Run(
t, t,
new(testSuite), new(quizTestSuite),
new(collectionTestSuite),
) )
} }
func (t *testSuite) TestQuizFromMarkdown() { func (t *quizTestSuite) TestQuizFromMarkdown() {
markdown := `Question text (1). markdown := `Question text (1).
Question text (2). Question text (2).
@ -55,8 +58,8 @@ Question text with #tag1 #tag2 (3).
} }
} }
func (t *testSuite) TestReadAllQuizzes() { func (t *quizTestSuite) TestReadAllQuizzes() {
store, err := NewFileProboCollectorStore("./testdata/quizzes") store, err := NewFileProboCollectorStore("./testdata/")
t.True(err == nil, fmt.Sprintf("A file store should be initialized without problems but an error occurred: %v", err)) t.True(err == nil, fmt.Sprintf("A file store should be initialized without problems but an error occurred: %v", err))
if !t.Failed() { 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)) store := memory.NewMemoryProboCollectorStore(sha256.NewDefault256Hasher(sha256.DefaultSHA256HashingFn))
quiz, err := store.CreateQuiz( quiz, err := store.CreateQuiz(
&client.CreateUpdateQuizRequest{ &client.CreateUpdateQuizRequest{
@ -102,9 +105,8 @@ func (t *testSuite) TestMarkdownFromQuiz() {
} }
} }
func (t *testSuite) TestCreateQuiz() { func (t *quizTestSuite) TestCreateQuiz() {
dirname := "./testdata/quizzes" store, err := NewFileProboCollectorStore(testdataDir)
store, err := NewFileProboCollectorStore(dirname)
t.True(err == nil, fmt.Sprintf("A file store should be initialized without problems but an error occurred: %v", err)) 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)) t.Nil(err, fmt.Sprintf("An error was raised when saving the quiz on disk: %v", err))
if !t.Failed() { if !t.Failed() {
path, err := store.GetPath(quiz) path, err := store.GetQuizPath(quiz)
t.Nil(err, "GetPath should not raise an error.") t.Nil(err, "GetPath should not raise an error.")
if !t.Failed() { if !t.Failed() {
@ -151,9 +153,8 @@ func (t *testSuite) TestCreateQuiz() {
} }
} }
func (t *testSuite) TestDeleteQuiz() { func (t *quizTestSuite) TestDeleteQuiz() {
dirname := "./testdata/quizzes" store, err := NewFileProboCollectorStore(testdataDir)
store, err := NewFileProboCollectorStore(dirname)
t.True(err == nil, fmt.Sprintf("A file store should be initialized without problems but an error occurred: %v", err)) t.True(err == nil, fmt.Sprintf("A file store should be initialized without problems but an error occurred: %v", err))
if !t.Failed() { if !t.Failed() {
@ -171,7 +172,7 @@ func (t *testSuite) TestDeleteQuiz() {
t.Nil(err, "The quiz to be deleted should be created without issue") 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") t.True(path != "", "Quiz path should be obtained without errors")
if !t.Failed() { if !t.Failed() {
@ -184,9 +185,8 @@ func (t *testSuite) TestDeleteQuiz() {
} }
} }
func (t *testSuite) TestUpdateQuiz() { func (t *quizTestSuite) TestUpdateQuiz() {
dirname := "./testdata/quizzes" store, err := NewFileProboCollectorStore(testdataDir)
store, err := NewFileProboCollectorStore(dirname)
t.True(err == nil, fmt.Sprintf("A file store should be initialized without problems but an error occurred: %v", err)) t.True(err == nil, fmt.Sprintf("A file store should be initialized without problems but an error occurred: %v", err))
if !t.Failed() { if !t.Failed() {
@ -225,7 +225,7 @@ func (t *testSuite) TestUpdateQuiz() {
t.True(len(updatedQuiz.Tags) == 1, "Length of tags array should be 1") t.True(len(updatedQuiz.Tags) == 1, "Length of tags array should be 1")
if !t.Failed() { if !t.Failed() {
path, err := store.GetPath(updatedQuiz) path, err := store.GetQuizPath(updatedQuiz)
if !t.Failed() { if !t.Failed() {
t.Nil(err, "GetPath should not raise an error.") t.Nil(err, "GetPath should not raise an error.")
@ -246,11 +246,9 @@ func (t *testSuite) TestUpdateQuiz() {
} }
func (t *testSuite) TestReadMetaHeaderFromFile() { func (t *quizTestSuite) TestReadMetaHeaderFromFile() {
dirname := "./testdata/quizzes" store, err := NewFileProboCollectorStore(testdataDir)
store, err := NewFileProboCollectorStore(dirname)
t.True(err == nil, fmt.Sprintf("A file store should be initialized without problems but an error occurred: %v", err)) 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") meta, err := store.ReadMetaHeaderFromFile("quiz_4.md")
t.True(err == nil, fmt.Sprintf("An error occurred: %v", err)) t.True(err == nil, fmt.Sprintf("An error occurred: %v", err))
if !t.Failed() { if !t.Failed() {
@ -259,9 +257,8 @@ func (t *testSuite) TestReadMetaHeaderFromFile() {
} }
} }
func (t *testSuite) TestWriteMetaHeaderToFile() { func (t *quizTestSuite) TestWriteMetaHeaderToFile() {
dirname := "./testdata/quizzes" store, err := NewFileProboCollectorStore(testdataDir)
store, err := NewFileProboCollectorStore(dirname)
t.True(err == nil, fmt.Sprintf("A file store should be initialized without problems but an error occurred: %v", err)) t.True(err == nil, fmt.Sprintf("A file store should be initialized without problems but an error occurred: %v", err))

View file

@ -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. This quiz is initially without metadata.
* Answer 1 * Answer 1

View file

@ -1,6 +1,7 @@
package memory package memory
import ( import (
"errors"
"fmt" "fmt"
"strings" "strings"
"sync" "sync"
@ -13,7 +14,7 @@ import (
type MemoryProboCollectorStore struct { type MemoryProboCollectorStore struct {
quizzes map[string]*models.Quiz quizzes map[string]*models.Quiz
collections map[string]*models.Collection
questionsHashes map[string]*models.Question questionsHashes map[string]*models.Question
answersHashes map[string]*models.Answer answersHashes map[string]*models.Answer
quizzesHashes map[string]*models.Quiz quizzesHashes map[string]*models.Quiz
@ -34,6 +35,7 @@ func NewMemoryProboCollectorStore(hasher hasher.Hasher) *MemoryProboCollectorSto
s.quizzesHashes = make(map[string]*models.Quiz) s.quizzesHashes = make(map[string]*models.Quiz)
s.quizzes = make(map[string]*models.Quiz) s.quizzes = make(map[string]*models.Quiz)
s.collections = make(map[string]*models.Collection)
return s return s
} }
@ -62,6 +64,18 @@ func (s *MemoryProboCollectorStore) getQuizFromID(id string) *models.Quiz {
return nil 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 { func (s *MemoryProboCollectorStore) getQuestionFromHash(hash string) *models.Question {
s.lock.RLock() s.lock.RLock()
defer s.lock.RUnlock() 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) { 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) hashes := s.hasher.QuizHashes(r.Quiz)
quizHash := hashes[len(hashes)-1] 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) { func (s *MemoryProboCollectorStore) DeleteQuiz(id string) (*models.Quiz, error) {
return s.deleteQuiz(id) 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
}

View file

@ -139,3 +139,62 @@ func (t *testSuite) TestDeleteQuiz() {
_, err = store.ReadQuizByHash(quiz.Hash) _, err = store.ReadQuizByHash(quiz.Hash)
t.True(err != nil, "Reading a non existent quiz should return an error") 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])
}
}
}

View file

@ -7,10 +7,14 @@ import (
type ProboCollectorStore interface { type ProboCollectorStore interface {
ReadAllQuizzes() ([]*models.Quiz, error) ReadAllQuizzes() ([]*models.Quiz, error)
ReadQuizByHash(hash string) (*models.Quiz, error) ReadQuizByHash(hash string) (*models.Quiz, error)
CreateQuiz(r *client.CreateUpdateQuizRequest) (*models.Quiz, error) CreateQuiz(r *client.CreateUpdateQuizRequest) (*models.Quiz, error)
UpdateQuiz(r *client.CreateUpdateQuizRequest, id string) (*models.Quiz, error) UpdateQuiz(r *client.CreateUpdateQuizRequest, id string) (*models.Quiz, error)
DeleteQuiz(r *client.DeleteQuizRequest) (*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)
} }