Ver código fonte

First implementation of SQlite store

andrea 6 meses atrás
pai
commit
578b4e2079

+ 39 - 0
client/client.go

@@ -21,6 +21,21 @@ type Collection struct {
 	Query string `json:"query"`
 }
 
+type Participant struct {
+	Firstname string `json:"firstname"`
+	Lastname  string `json:"lastname"`
+	Class     string `json:"class"`
+	Token     uint   `json:"token"`
+}
+
+type Exam struct {
+	Name        string `json:"name"`
+	Description string `json:"description"`
+
+	ParticipantID string `json:"participant_id"`
+	CollectionID  string `json:"collection_id"`
+}
+
 type BaseResponse struct {
 	Status  string `json:"status"`
 	Message string `json:"message"`
@@ -65,3 +80,27 @@ type CreateUpdateCollectionRequest struct {
 type DeleteCollectionRequest struct {
 	ID string
 }
+
+type CreateUpdateExamRequest struct {
+	*Exam
+}
+
+type DeleteExamRequest struct {
+	ID string
+}
+
+type ReadExamByIDRequest struct {
+	ID string
+}
+
+type CreateUpdateParticipantRequest struct {
+	*Participant
+}
+
+type DeleteParticipantRequest struct {
+	ID string
+}
+
+type ReadParticipantByIDRequest struct {
+	ID string
+}

+ 14 - 2
go.mod

@@ -5,12 +5,24 @@ go 1.17
 require github.com/sirupsen/logrus v1.8.1
 
 require (
+	github.com/dustin/go-humanize v1.0.1 // indirect
+	github.com/glebarez/go-sqlite v1.21.2 // indirect
+	github.com/glebarez/sqlite v1.9.0 // indirect
 	github.com/go-yaml/yaml v2.1.0+incompatible // indirect
-	github.com/google/uuid v1.3.0 // indirect
+	github.com/google/uuid v1.3.1 // indirect
+	github.com/jinzhu/inflection v1.0.0 // 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/text v0.1.0 // indirect
+	github.com/mattn/go-isatty v0.0.20 // indirect
 	github.com/remogatto/prettytest v0.0.0-20200211072524-6d385e11dcb8 // indirect
-	golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 // indirect
+	github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
+	golang.org/x/sys v0.13.0 // 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/mathutil v1.6.0 // indirect
+	modernc.org/memory v1.7.2 // indirect
+	modernc.org/sqlite v1.26.0 // indirect
 )

+ 29 - 0
go.sum

@@ -1,8 +1,20 @@
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
+github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
+github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
+github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
+github.com/glebarez/sqlite v1.9.0 h1:Aj6bPA12ZEx5GbSF6XADmCkYXlljPNUY+Zf1EQxynXs=
+github.com/glebarez/sqlite v1.9.0/go.mod h1:YBYCoyupOao60lzp1MVBLEjZfgkq0tdB1voAQ09K9zw=
 github.com/go-yaml/yaml v2.1.0+incompatible h1:RYi2hDdss1u4YE7GwixGzWwVo47T8UQwnTLB6vQiq+o=
 github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
 github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
 github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
+github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
+github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
+github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
+github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
 github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
 github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
 github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
@@ -10,13 +22,30 @@ github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/remogatto/prettytest v0.0.0-20200211072524-6d385e11dcb8 h1:nRDwTcxV9B3elxMt+1xINX0bwaPdpouqp5fbynexY8U=
 github.com/remogatto/prettytest v0.0.0-20200211072524-6d385e11dcb8/go.mod h1:jOEnp79oIHy5cvQSHeLcgVJk1GHOOHJHQWps/d1N5Yo=
+github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
+github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
 github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
 github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
 golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
+golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
+gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
+modernc.org/libc v1.24.1 h1:uvJSeCKL/AgzBo2yYIPPTy82v21KgGnizcGYfBHaNuM=
+modernc.org/libc v1.24.1/go.mod h1:FmfO1RLrU3MHJfyi9eYYmZBfi/R+tqZ6+hQ3yQQUkak=
+modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
+modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
+modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
+modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
+modernc.org/sqlite v1.26.0 h1:SocQdLRSYlA8W99V8YH0NES75thx19d9sB/aFc4R8Lw=
+modernc.org/sqlite v1.26.0/go.mod h1:FL3pVXie73rg3Rii6V/u5BoHlSoyeZeIgKZEgHARyCU=

+ 1 - 1
models/answer.go

@@ -1,6 +1,6 @@
 package models
 
 type Answer struct {
-	ID   string `json:"id"`
+	ID   string `json:"id" gorm:"primaryKey"`
 	Text string `json:"text"`
 }

+ 3 - 2
models/collection.go

@@ -1,9 +1,10 @@
 package models
 
 type Collection struct {
-	ID    string `json:"id"`
+	Meta
+
 	Name  string `json:"name"`
 	Query string `json:"query"`
 
-	IDs []string `json:"ids"`
+	Quizzes []*Quiz `json:"quizzes" gorm:"many2many:collection_quizzes"`
 }

+ 15 - 5
models/exam.go

@@ -1,13 +1,23 @@
 package models
 
-import "time"
+import (
+	"time"
+
+	"gorm.io/gorm"
+)
 
 type Exam struct {
-	ID        string `gorm:"primaryKey"`
+	ID string `gorm:"primaryKey"`
+
 	CreatedAt time.Time
+	UpdatedAt time.Time
+	DeletedAt gorm.DeletedAt `gorm:"index"`
+
+	Name        string
+	Description string
 
-	Quizzes     []*Quiz
-	Participant *Participant
+	Collection  *Collection    `gorm:"foreignKey:ID"`
+	Participant []*Participant `gorm:"many2many:exam_participants"`
 
-	Responses []string
+	//	Responses []string
 }

+ 3 - 2
models/meta.go

@@ -3,7 +3,8 @@ package models
 import "time"
 
 type Meta struct {
-	ID        string    `json:"id" yaml:"id"`
+	ID        string    `json:"id" yaml:"id" gorm:"primaryKey"`
 	CreatedAt time.Time `json:"created_at" yaml:"created_at"`
-	Tags      []*Tag    `json:"tags" yaml:"-"`
+	UpdatedAt time.Time `json:"updated_at" yaml:"updated_at"`
+	Tags      []*Tag    `json:"tags" yaml:"-" gorm:"-"`
 }

+ 1 - 1
models/participant.go

@@ -1,7 +1,7 @@
 package models
 
 type Participant struct {
-	ID string
+	ID string `gorm:"primaryKey"`
 
 	Firstname string
 	Lastname  string

+ 2 - 3
models/question.go

@@ -1,7 +1,6 @@
 package models
 
 type Question struct {
-	ID        string   `json:"id"`
-	Text      string   `json:"text"`
-	AnswerIDs []string `json:"answer_ids"`
+	Meta
+	Text string `json:"text"`
 }

+ 4 - 3
models/quiz.go

@@ -2,9 +2,10 @@ package models
 
 type Quiz struct {
 	Meta
+
 	Hash     string    `json:"hash"`
-	Question *Question `json:"question"`
-	Answers  []*Answer `json:"answers"`
-	Correct  *Answer   `json:"correct"`
+	Question *Question `json:"question" gorm:"foreignKey:ID"`
+	Answers  []*Answer `json:"answers" gorm:"many2many:quiz_answers"`
+	Correct  *Answer   `json:"correct" gorm:"foreignKey:ID"`
 	Type     int       `json:"type"`
 }

+ 10 - 1
models/tag.go

@@ -1,5 +1,14 @@
 package models
 
+import (
+	"time"
+
+	"gorm.io/gorm"
+)
+
 type Tag struct {
-	Name string `json:"name"`
+	CreatedAt time.Time
+	UpdatedAt time.Time
+	DeletedAt gorm.DeletedAt `gorm:"index"`
+	Name      string         `json:"name" gorm:"primaryKey"`
 }

+ 72 - 2
store/db/db.go

@@ -1,9 +1,79 @@
 package db
 
+import (
+	"git.andreafazzi.eu/andrea/probo/client"
+	"git.andreafazzi.eu/andrea/probo/models"
+	"git.andreafazzi.eu/andrea/probo/store"
+	"github.com/glebarez/sqlite"
+	"github.com/google/uuid"
+	"gorm.io/gorm"
+)
+
 type DBProboCollectorStore struct {
 	Path string
+
+	db *gorm.DB
+	si store.ProboCollectorStore
+}
+
+func NewDBProboCollectorStore(path string, s store.ProboCollectorStore) (*DBProboCollectorStore, error) {
+	var err error
+
+	store := new(DBProboCollectorStore)
+
+	store.db, err = gorm.Open(sqlite.Open(path), &gorm.Config{})
+	if err != nil {
+		return nil, err
+	}
+
+	err = store.db.AutoMigrate(
+		&models.Question{},
+		&models.Answer{},
+		&models.Quiz{},
+		&models.Collection{},
+		&models.Exam{},
+		&models.Participant{},
+	)
+	if err != nil {
+		return nil, err
+	}
+
+	store.si = s
+
+	return store, nil
 }
 
-func NewDBProboCollectorStore(path string) (*DBProboCollectorStore, error) {
-	return nil, nil
+func (s *DBProboCollectorStore) CreateExam(r *client.CreateUpdateExamRequest) (*models.Exam, error) {
+	exam := new(models.Exam)
+
+	exam.ID = uuid.New().String()
+	exam.Name = r.Name
+	exam.Description = r.Description
+
+	collection, err := s.si.ReadCollectionByID(r.CollectionID)
+	if err != nil {
+		return nil, err
+	}
+
+	exam.Collection = collection
+
+	result := s.db.Create(exam)
+	if result.Error != nil {
+		return nil, result.Error
+	}
+
+	return exam, nil
+}
+
+func (s *DBProboCollectorStore) ReadExamByID(r *client.ReadExamByIDRequest) (*models.Exam, error) {
+	exam := &models.Exam{
+		ID: r.ID,
+	}
+
+	s.db.First(&exam)
+	if err := s.db.Error; err != nil {
+		return nil, err
+	}
+
+	return exam, nil
 }

+ 54 - 2
store/db/db_test.go

@@ -1,8 +1,12 @@
 package db
 
 import (
+	"os"
 	"testing"
 
+	"git.andreafazzi.eu/andrea/probo/client"
+	"git.andreafazzi.eu/andrea/probo/hasher/sha256"
+	"git.andreafazzi.eu/andrea/probo/store/memory"
 	"github.com/remogatto/prettytest"
 )
 
@@ -18,7 +22,55 @@ func TestRunner(t *testing.T) {
 }
 
 func (t *dbTestSuite) TestCreateExam() {
-	store, err := NewDBProboCollectorStore("testdata/test.sqlite")
+	memStore := memory.NewMemoryProboCollectorStore(
+		sha256.NewDefault256Hasher(sha256.DefaultSHA256HashingFn),
+	)
+	memStore.CreateQuiz(
+		&client.CreateUpdateQuizRequest{
+			Quiz: &client.Quiz{
+				Question: &client.Question{Text: "Newly created question text with #tag1."},
+				Answers: []*client.Answer{
+					{Text: "Answer 1", Correct: true},
+					{Text: "Answer 2", Correct: false},
+					{Text: "Answer 3", Correct: false},
+					{Text: "Answer 4", Correct: false},
+				},
+			},
+		})
+
+	store, err := NewDBProboCollectorStore("testdata/test.sqlite", memStore)
+	defer os.Remove("testdata/test.sqlite")
+
 	t.Nil(err)
-	t.Not(t.Nil(store))
+
+	if !t.Failed() {
+		collection, err := memStore.CreateCollection(&client.CreateUpdateCollectionRequest{
+			Collection: &client.Collection{
+				Name:  "Collection name",
+				Query: "#tag1",
+			},
+		})
+
+		t.Nil(err)
+
+		if !t.Failed() {
+			exam, err := store.CreateExam(&client.CreateUpdateExamRequest{
+				Exam: &client.Exam{
+					Name:         "Exam",
+					Description:  "Exam description",
+					CollectionID: collection.ID,
+				},
+			})
+
+			t.Nil(err)
+			if !t.Failed() {
+				t.Not(t.Nil(exam))
+				examFromDb, err := store.ReadExamByID(&client.ReadExamByIDRequest{ID: exam.ID})
+				t.Nil(err)
+				if !t.Failed() {
+					t.Equal(examFromDb.ID, exam.ID)
+				}
+			}
+		}
+	}
 }

+ 1 - 1
store/file/collection_test.go

@@ -86,7 +86,7 @@ func (t *collectionTestSuite) TestCreateCollection() {
 					collectionPath, _ := store.GetCollectionPath(collection)
 
 					if !t.Failed() {
-						t.Equal(2, len(collection.IDs))
+						t.Equal(2, len(collection.Quizzes))
 
 						os.Remove(path_1)
 						os.Remove(path_2)

+ 0 - 615
store/file/file.go

@@ -1,26 +1,12 @@
 package file
 
 import (
-	"bufio"
-	"bytes"
-	"encoding/json"
 	"errors"
-	"fmt"
-	"io"
-	"io/fs"
-	"io/ioutil"
-	"os"
-	"path"
 	"path/filepath"
-	"strings"
 	"sync"
-	"time"
 
-	"git.andreafazzi.eu/andrea/probo/client"
 	"git.andreafazzi.eu/andrea/probo/hasher/sha256"
-	"git.andreafazzi.eu/andrea/probo/models"
 	"git.andreafazzi.eu/andrea/probo/store/memory"
-	"github.com/go-yaml/yaml"
 )
 
 var (
@@ -61,109 +47,6 @@ func NewFileProboCollectorStore(dirname string) (*FileProboCollectorStore, error
 	return s, nil
 }
 
-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
-	}
-
-	markdownFiles := make([]fs.FileInfo, 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) 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),
@@ -184,501 +67,3 @@ func (s *FileProboCollectorStore) Reindex() error {
 
 	return nil
 }
-
-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(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 (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.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
-}
-
-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 := ioutil.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) 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) 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 (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
-
-	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
-}

+ 3 - 2
store/file/testdata/quizzes/quiz_5.md

@@ -1,6 +1,7 @@
 ---
-id: e4ee7eb2-5608-4709-ad85-1381b457de35
-created_at: !!timestamp 2023-10-16T12:47:06.100160075+02:00
+id: 9243c924-5a91-4fa1-9cf3-db9ce1e19ca4
+created_at: !!timestamp 2023-10-28T20:49:22.688075744+02:00
+updated_at: !!timestamp 0001-01-01T00:00:00Z
 ---
 This quiz is initially without metadata.
 

+ 46 - 14
store/memory/memory.go

@@ -140,21 +140,30 @@ func (s *MemoryProboCollectorStore) deleteQuiz(id string) (*models.Quiz, error)
 		return nil, fmt.Errorf("Trying to delete a quiz that doesn't exist in memory (ID: %v)", id)
 	}
 
-	s.quizzes[id] = nil
-	s.quizzesHashes[quiz.Hash] = nil
+	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 {
-		result = append(result, s.getQuizFromID(id))
+		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 {
@@ -236,7 +245,7 @@ func (s *MemoryProboCollectorStore) createOrUpdateQuiz(r *client.CreateUpdateQui
 	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{
-			ID:   uuid.New().String(),
+			Meta: models.Meta{ID: uuid.New().String()},
 			Text: s.parseTextForTags(r.Quiz.Question.Text, &quiz.Tags),
 		})
 	}
@@ -274,14 +283,16 @@ func (s *MemoryProboCollectorStore) UpdateQuiz(r *client.CreateUpdateQuizRequest
 	return s.createOrUpdateQuiz(r, id)
 }
 
-func (s *MemoryProboCollectorStore) DeleteQuiz(id string) (*models.Quiz, error) {
-	return s.deleteQuiz(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 {
-		result = append(result, s.getCollectionFromID(id))
+		if collection := s.getCollectionFromID(id); collection != nil {
+			result = append(result, collection)
+		}
 	}
 	return result, nil
 }
@@ -296,10 +307,31 @@ func (s *MemoryProboCollectorStore) UpdateCollection(r *client.CreateUpdateColle
 }
 
 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", collection.ID)
+		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
 }
 
@@ -323,21 +355,21 @@ func (s *MemoryProboCollectorStore) createOrUpdateCollection(r *client.CreateUpd
 	collection.Name = r.Collection.Name
 	collection.Query = r.Collection.Query
 
-	collection.IDs = s.query(collection.Query)
+	collection.Quizzes = s.query(collection.Query)
 
 	return s.createCollectionFromID(id, collection), true, nil
 }
 
-func (s *MemoryProboCollectorStore) query(query string) []string {
+func (s *MemoryProboCollectorStore) query(query string) []*models.Quiz {
 	s.lock.Lock()
 	defer s.lock.Unlock()
 
-	result := make([]string, 0)
+	result := make([]*models.Quiz, 0)
 
-	for id, quiz := range s.quizzes {
+	for _, quiz := range s.quizzes {
 		for _, tag := range quiz.Tags {
 			if query == tag.Name {
-				result = append(result, id)
+				result = append(result, quiz)
 				break
 			}
 		}

+ 3 - 61
store/memory/memory_test.go

@@ -18,6 +18,7 @@ func TestRunner(t *testing.T) {
 	prettytest.Run(
 		t,
 		new(testSuite),
+		new(collectionTestSuite),
 	)
 }
 
@@ -131,70 +132,11 @@ func (t *testSuite) TestDeleteQuiz() {
 			},
 		})
 
-	deletedQuiz, err := store.DeleteQuiz(quiz.ID)
+	deletedQuiz, err := store.DeleteQuiz(&client.DeleteQuizRequest{ID: quiz.ID})
 
 	t.Equal(quiz.ID, deletedQuiz.ID, "Returned deleted quiz ID should be equal to the request")
 	t.Nil(err, fmt.Sprintf("The update returned an error: %v", err))
 
-	_, err = store.ReadQuizByHash(quiz.Hash)
+	_, err = store.ReadQuizByHash(deletedQuiz.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])
-		}
-	}
-}

+ 43 - 3
store/store.go

@@ -5,16 +5,56 @@ import (
 	"git.andreafazzi.eu/andrea/probo/models"
 )
 
-type ProboCollectorStore interface {
+type QuizReader interface {
 	ReadAllQuizzes() ([]*models.Quiz, error)
+	ReadQuizByID(id string) (*models.Quiz, error)
 	ReadQuizByHash(hash string) (*models.Quiz, error)
+}
+
+type QuizWriter interface {
 	CreateQuiz(r *client.CreateUpdateQuizRequest) (*models.Quiz, error)
-	UpdateQuiz(r *client.CreateUpdateQuizRequest, id string) (*models.Quiz, error)
+	UpdateQuiz(r *client.CreateUpdateQuizRequest, id string) (*models.Quiz, bool, error)
 	DeleteQuiz(r *client.DeleteQuizRequest) (*models.Quiz, error)
+}
 
+type CollectionReader interface {
 	ReadAllCollections() ([]*models.Collection, error)
 	ReadCollectionByID(id string) (*models.Collection, error)
+}
+
+type CollectionWriter interface {
 	CreateCollection(r *client.CreateUpdateCollectionRequest) (*models.Collection, error)
-	UpdateCollection(r *client.CreateUpdateCollectionRequest, id string) (*models.Collection, error)
+	UpdateCollection(r *client.CreateUpdateCollectionRequest, id string) (*models.Collection, bool, error)
 	DeleteCollection(r *client.DeleteCollectionRequest) (*models.Collection, error)
 }
+
+type ParticipantReader interface {
+	ReadAllParticipants() ([]*models.Participant, error)
+	ReadParticipantByID(id string) (*models.Participant, error)
+}
+
+type ParticipantWriter interface {
+	CreateParticipant(r *client.CreateUpdateParticipantRequest) (*models.Participant, error)
+	UpdateParticipant(r *client.CreateUpdateParticipantRequest, id string) (*models.Participant, bool, error)
+	DeleteParticipant(r *client.DeleteParticipantRequest) (*models.Participant, error)
+}
+
+type ExamReader interface {
+	ReadAllExams() ([]*models.Exam, error)
+	ReadExamByID(id string) (*models.Exam, error)
+}
+
+type ExamWriter interface {
+	CreateExam(r *client.CreateUpdateExamRequest) (*models.Exam, error)
+	UpdateExam(r *client.CreateUpdateExamRequest, id string) (*models.Exam, bool, error)
+	DeleteExam(r *client.DeleteExamRequest) (*models.Exam, error)
+}
+
+type ProboCollectorStore interface {
+	QuizReader
+	QuizWriter
+	CollectionReader
+	CollectionWriter
+	ParticipantReader
+	ParticipantWriter
+}