Last commit before switching to generics

This commit is contained in:
andrea 2023-11-05 14:36:33 +01:00
parent 578b4e2079
commit ce9dd7fd63
15 changed files with 1329 additions and 354 deletions

View file

@ -24,8 +24,8 @@ type Collection struct {
type Participant struct { type Participant struct {
Firstname string `json:"firstname"` Firstname string `json:"firstname"`
Lastname string `json:"lastname"` Lastname string `json:"lastname"`
Class string `json:"class"`
Token uint `json:"token"` Token uint `json:"token"`
Attributes map[string]string `json:"attributes"`
} }
type Exam struct { type Exam struct {

12
go.mod
View file

@ -1,18 +1,21 @@
module git.andreafazzi.eu/andrea/probo module git.andreafazzi.eu/andrea/probo
go 1.17 go 1.21
require github.com/sirupsen/logrus v1.8.1 require (
github.com/google/uuid v1.3.1
github.com/julienschmidt/httprouter v1.3.0
github.com/sirupsen/logrus v1.8.1
gorm.io/gorm v1.25.5
)
require ( require (
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/glebarez/go-sqlite v1.21.2 // indirect github.com/glebarez/go-sqlite v1.21.2 // indirect
github.com/glebarez/sqlite v1.9.0 // indirect github.com/glebarez/sqlite v1.9.0 // indirect
github.com/go-yaml/yaml v2.1.0+incompatible // indirect github.com/go-yaml/yaml v2.1.0+incompatible // indirect
github.com/google/uuid v1.3.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
github.com/julienschmidt/httprouter v1.3.0 // indirect
github.com/kr/pretty v0.2.1 // indirect github.com/kr/pretty v0.2.1 // indirect
github.com/kr/text v0.1.0 // indirect github.com/kr/text v0.1.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
@ -20,7 +23,6 @@ require (
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
golang.org/x/sys v0.13.0 // indirect golang.org/x/sys v0.13.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gorm.io/gorm v1.25.5 // indirect
modernc.org/libc v1.24.1 // indirect modernc.org/libc v1.24.1 // indirect
modernc.org/mathutil v1.6.0 // indirect modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.7.2 // indirect modernc.org/memory v1.7.2 // indirect

View file

@ -52,3 +52,4 @@ func (h *Default256Hasher) Calculate(hashes []string) string {
return h.hashFn(strings.Join(orderedHashes, "")) return h.hashFn(strings.Join(orderedHashes, ""))
} }
-

6
models/group.go Normal file
View file

@ -0,0 +1,6 @@
package models
type Group struct {
Name string
Participants []*Participant
}

View file

@ -5,4 +5,8 @@ type Participant struct {
Firstname string Firstname string
Lastname string Lastname string
Token uint
Attributes map[string]string
} }

160
store/file/collection.go Normal file
View file

@ -0,0 +1,160 @@
package file
import (
"encoding/json"
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"strings"
"git.andreafazzi.eu/andrea/probo/client"
"git.andreafazzi.eu/andrea/probo/models"
)
func (s *FileProboCollectorStore) GetCollectionsDir() string {
return s.collectionsDir
}
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) SetCollectionPath(collection *models.Collection, path string) string {
s.lock.Lock()
defer s.lock.Unlock()
s.collectionsPaths[collection.ID] = path
return path
}
func (s *FileProboCollectorStore) ReadAllCollections() ([]*models.Collection, error) {
return s.memoryStore.ReadAllCollections()
}
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) UpdateCollection(r *client.CreateUpdateCollectionRequest, id string) (*models.Collection, error) {
collection, _, err := s.memoryStore.UpdateCollection(r, id)
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) DeleteCollection(r *client.DeleteCollectionRequest) (*models.Collection, error) {
collection, err := s.memoryStore.DeleteCollection(&client.DeleteCollectionRequest{ID: r.ID})
if err != nil {
return nil, err
}
path, err := s.GetCollectionPath(collection)
if err != nil {
return nil, err
}
err = os.Remove(path)
if err != nil {
return nil, err
}
return collection, nil
}
func (s *FileProboCollectorStore) reindexCollections() error {
files, err := os.ReadDir(s.collectionsDir)
if err != nil {
return err
}
jsonFiles := make([]fs.DirEntry, 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) 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
}

494
store/file/quiz.go Normal file
View file

@ -0,0 +1,494 @@
package file
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"io/fs"
"os"
"path"
"path/filepath"
"strings"
"time"
"git.andreafazzi.eu/andrea/probo/client"
"git.andreafazzi.eu/andrea/probo/models"
"github.com/go-yaml/yaml"
)
func (s *FileProboCollectorStore) GetQuizzesDir() string {
return s.quizzesDir
}
func (s *FileProboCollectorStore) SetQuizPath(quiz *models.Quiz, path string) string {
s.lock.Lock()
defer s.lock.Unlock()
s.quizzesPaths[quiz.ID] = path
return path
}
func (s *FileProboCollectorStore) ReadAllQuizzes() ([]*models.Quiz, error) {
return s.memoryStore.ReadAllQuizzes()
}
func (s *FileProboCollectorStore) CreateQuiz(r *client.CreateUpdateQuizRequest) (*models.Quiz, error) {
quiz, err := s.memoryStore.CreateQuiz(r)
if err != nil {
return nil, err
}
err = s.createOrUpdateMarkdownFile(quiz)
if err != nil {
return nil, err
}
err = s.Reindex()
if err != nil {
return nil, err
}
return s.memoryStore.ReadQuizByHash(quiz.Hash)
}
func (s *FileProboCollectorStore) UpdateQuiz(r *client.CreateUpdateQuizRequest, id string) (*models.Quiz, error) {
quiz, updated, err := s.memoryStore.UpdateQuiz(r, id)
if err != nil {
return nil, err
}
if updated { // Update and re-index only if quiz hash is changed
err = s.createOrUpdateMarkdownFile(quiz)
if err != nil {
return nil, err
}
err = s.Reindex()
if err != nil {
return nil, err
}
}
return s.memoryStore.ReadQuizByHash(quiz.Hash)
}
func (s *FileProboCollectorStore) DeleteQuiz(r *client.DeleteQuizRequest) (*models.Quiz, error) {
quiz, err := s.memoryStore.DeleteQuiz(&client.DeleteQuizRequest{ID: r.ID})
if err != nil {
return nil, err
}
path, err := s.GetQuizPath(quiz)
if err != nil {
return nil, err
}
err = os.Remove(path)
if err != nil {
return nil, err
}
err = s.Reindex()
if err != nil {
return nil, err
}
return quiz, nil
}
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.quizzesPaths[quiz.ID]
if !ok {
return "", errors.New(fmt.Sprintf("Path not found for quiz ID %v", quiz.ID))
}
return path, nil
}
func MarkdownFromQuiz(quiz *models.Quiz) (string, error) {
if quiz.Question == nil {
return "", errors.New("Quiz should contain a question but it wasn't provided.")
}
if len(quiz.Answers) < 2 {
return "", errors.New("Quiz should contain at least 2 answers but none was provided.")
}
if quiz.Correct == nil {
return "", errors.New("Quiz should contain a correct answer but not was provided.")
}
correctAnswer := "* " + quiz.Correct.Text
var otherAnswers string
for _, answer := range quiz.Answers {
if quiz.Correct.ID != answer.ID {
otherAnswers += "* " + answer.Text + "\n"
}
}
markdown := quiz.Question.Text + "\n\n" + correctAnswer + "\n" + otherAnswers
return markdown, nil
}
func QuizFromMarkdown(markdown string) (*client.Quiz, *models.Meta, error) {
meta, remainingMarkdown, err := parseMetaHeaderFromMarkdown(markdown)
if err != nil {
return nil, nil, err
}
lines := strings.Split(remainingMarkdown, "\n")
questionText := ""
answers := []*client.Answer{}
for _, line := range lines {
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 (s *FileProboCollectorStore) ReadMetaHeaderFromFile(filename string) (*models.Meta, error) {
data, err := os.ReadFile(path.Join(s.quizzesDir, filename))
if err != nil {
return nil, err
}
meta, _, err := parseMetaHeaderFromMarkdown(string(data))
if err != nil {
return nil, err
}
return meta, nil
}
func (s *FileProboCollectorStore) WriteMetaHeaderToFile(filename string, meta *models.Meta) (*models.Meta, error) {
readMeta, err := s.ReadMetaHeaderFromFile(filename)
if err != nil {
return nil, err
}
if readMeta == nil {
_, err := writeMetaHeader(path.Join(s.quizzesDir, filename), meta)
if err != nil {
return nil, err
}
}
return meta, nil
}
func (s *FileProboCollectorStore) reindexQuizzes() error {
files, err := os.ReadDir(s.quizzesDir)
if err != nil {
return err
}
markdownFiles := make([]fs.DirEntry, 0)
for _, file := range files {
filename := file.Name()
if !file.IsDir() && strings.HasSuffix(filename, ".md") {
markdownFiles = append(markdownFiles, file)
}
}
if len(markdownFiles) == 0 {
return fmt.Errorf("The directory is empty.")
}
for _, file := range markdownFiles {
filename := file.Name()
fullPath := filepath.Join(s.quizzesDir, filename)
content, err := os.ReadFile(fullPath)
if err != nil {
return err
}
quiz, meta, err := QuizFromMarkdown(string(content))
if err != nil {
return err
}
q, err := s.memoryStore.CreateQuiz(&client.CreateUpdateQuizRequest{
Quiz: quiz,
Meta: meta,
})
if err != nil {
return err
}
if meta == nil {
s.WriteMetaHeaderToFile(filename, &models.Meta{
ID: q.ID,
CreatedAt: time.Now(),
})
}
s.SetQuizPath(q, fullPath)
}
return nil
}
func (s *FileProboCollectorStore) removeMetaFromFile(filename string) (*models.Meta, error) {
file, err := os.Open(path.Join(s.quizzesDir, filename))
if err != nil {
return nil, err
}
defer file.Close()
var buffer bytes.Buffer
reader := bufio.NewReader(file)
var meta models.Meta
var line string
var sb strings.Builder
for {
line, err = reader.ReadString('\n')
if err != nil {
if err == io.EOF {
break
}
return nil, err
}
if strings.TrimSpace(line) == "---" {
break
}
}
for {
line, err = reader.ReadString('\n')
if err != nil {
if err == io.EOF {
break
}
return nil, err
}
if strings.TrimSpace(line) == "---" {
break
}
sb.WriteString(line)
}
err = yaml.Unmarshal([]byte(sb.String()), &meta)
if err != nil {
return nil, err
}
_, err = io.Copy(&buffer, reader)
if err != nil {
return nil, err
}
file, err = os.Create(path.Join(s.quizzesDir, filename))
if err != nil {
return nil, err
}
defer file.Close()
_, err = io.Copy(file, &buffer)
if err != nil {
return nil, err
}
return &meta, nil
}
func (s *FileProboCollectorStore) createOrUpdateMarkdownFile(quiz *models.Quiz) error {
markdown, err := MarkdownFromQuiz(quiz)
if err != nil {
return err
}
fn, _ := s.GetQuizPath(quiz)
if fn == "" {
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)
if err != nil {
return err
}
_, err = file.Write([]byte(markdownWithMetaHeader))
if err != nil {
return err
}
s.SetQuizPath(quiz, fn)
return nil
}
func addMetaHeaderToMarkdown(content string, meta *models.Meta) (string, error) {
var buffer bytes.Buffer
header, err := yaml.Marshal(meta)
if err != nil {
return "", err
}
_, err = buffer.WriteString("---\n" + string(header) + "---\n")
if err != nil {
return "", err
}
_, err = buffer.WriteString(content)
if err != nil {
return "", err
}
return buffer.String(), nil
}
func parseMetaHeaderFromMarkdown(markdown string) (*models.Meta, string, error) {
reader := strings.NewReader(markdown)
var sb strings.Builder
var line string
var err error
for {
line, err = readLine(reader)
if err != nil {
if err == io.EOF {
break
}
return nil, "", err
}
if strings.TrimSpace(line) == "---" {
break
}
}
for {
line, err = readLine(reader)
if err != nil {
if err == io.EOF {
break
}
return nil, "", err
}
if strings.TrimSpace(line) == "---" {
break
}
sb.WriteString(line)
}
if sb.String() == "" {
return nil, markdown, nil
}
var meta models.Meta
err = yaml.Unmarshal([]byte(sb.String()), &meta)
if err != nil {
return nil, markdown, err
}
remainingMarkdown := markdown[strings.Index(markdown, "---\n"+sb.String()+"---\n")+len("---\n"+sb.String()+"---\n"):]
return &meta, remainingMarkdown, nil
}
func readLine(reader *strings.Reader) (string, error) {
var sb strings.Builder
for {
r, _, err := reader.ReadRune()
if err != nil {
if err == io.EOF {
return sb.String(), io.EOF
}
return "", err
}
sb.WriteRune(r)
if r == '\n' {
break
}
}
return sb.String(), nil
}
func writeMetaHeader(filename string, meta *models.Meta) (*models.Meta, error) {
// Apri il file in lettura
file, err := os.Open(filename)
if err != nil {
return nil, err
}
defer file.Close()
// Crea un buffer in memoria
var buffer bytes.Buffer
// Scrivi l'intestazione YAML nel buffer
header, err := yaml.Marshal(meta)
if err != nil {
return nil, err
}
_, err = buffer.WriteString("---\n" + string(header) + "---\n")
if err != nil {
return nil, err
}
// Copia il contenuto del file originale nel buffer
_, err = io.Copy(&buffer, file)
if err != nil {
return nil, err
}
// Riapri il file in scrittura
file, err = os.Create(filename)
if err != nil {
return nil, err
}
defer file.Close()
// Scrivi il contenuto del buffer nel file
_, err = io.Copy(file, &buffer)
if err != nil {
return nil, err
}
return meta, nil
}

View file

@ -1,6 +1,6 @@
--- ---
id: 9243c924-5a91-4fa1-9cf3-db9ce1e19ca4 id: b7ec3eb9-55e1-47c6-8652-3a27fe90bc0f
created_at: !!timestamp 2023-10-28T20:49:22.688075744+02:00 created_at: !!timestamp 2023-10-31T10:02:02.869395215+01:00
updated_at: !!timestamp 0001-01-01T00:00:00Z updated_at: !!timestamp 0001-01-01T00:00:00Z
--- ---
This quiz is initially without metadata. This quiz is initially without metadata.

124
store/memory/collection.go Normal file
View file

@ -0,0 +1,124 @@
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
}

View file

@ -0,0 +1,98 @@
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")
}

View file

@ -2,19 +2,20 @@ package memory
import ( import (
"errors" "errors"
"fmt"
"strings"
"sync" "sync"
"git.andreafazzi.eu/andrea/probo/client"
"git.andreafazzi.eu/andrea/probo/hasher" "git.andreafazzi.eu/andrea/probo/hasher"
"git.andreafazzi.eu/andrea/probo/models" "git.andreafazzi.eu/andrea/probo/models"
"github.com/google/uuid"
) )
type MemoryProboCollectorStore struct { type MemoryProboCollectorStore struct {
// IDs maps
quizzes map[string]*models.Quiz quizzes map[string]*models.Quiz
collections map[string]*models.Collection collections map[string]*models.Collection
participants map[string]*models.Participant
// Hashes maps
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
@ -36,181 +37,17 @@ func NewMemoryProboCollectorStore(hasher hasher.Hasher) *MemoryProboCollectorSto
s.quizzes = make(map[string]*models.Quiz) s.quizzes = make(map[string]*models.Quiz)
s.collections = make(map[string]*models.Collection) s.collections = make(map[string]*models.Collection)
s.participants = make(map[string]*models.Participant)
return s return s
} }
func (s *MemoryProboCollectorStore) getQuizFromHash(hash string) *models.Quiz { func Create[T any](s *MemoryProboCollectorStore, elem *T) (*T, error) {
s.lock.RLock() if elem == nil {
defer s.lock.RUnlock() return nil, errors.New("A creation request was made passing a nil element")
quiz, ok := s.quizzesHashes[hash]
if ok {
return quiz
} }
return nil // Check for duplicates
}
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) 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()
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
}
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) 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) hashes := s.hasher.QuizHashes(r.Quiz)
quizHash := hashes[len(hashes)-1] quizHash := hashes[len(hashes)-1]
@ -219,172 +56,4 @@ func (s *MemoryProboCollectorStore) createOrUpdateQuiz(r *client.CreateUpdateQui
return quiz, false, nil 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) 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) 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
} }

View file

@ -19,6 +19,7 @@ func TestRunner(t *testing.T) {
t, t,
new(testSuite), new(testSuite),
new(collectionTestSuite), new(collectionTestSuite),
new(participantTestSuite),
) )
} }

108
store/memory/participant.go Normal file
View file

@ -0,0 +1,108 @@
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
}

View file

@ -0,0 +1,62 @@
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")
}

246
store/memory/quiz.go Normal file
View file

@ -0,0 +1,246 @@
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
}