Sfoglia il codice sorgente

Completed first refactoring with Go generics

andrea 6 mesi fa
parent
commit
3cdfa72403

+ 1 - 0
go.mod

@@ -15,6 +15,7 @@ require (
 	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/gocarina/gocsv v0.0.0-20230616125104-99d496ca653d // indirect
 	github.com/jinzhu/inflection v1.0.0 // indirect
 	github.com/jinzhu/now v1.1.5 // indirect
 	github.com/kr/pretty v0.2.1 // indirect

+ 3 - 0
go.sum

@@ -7,6 +7,8 @@ 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/gocarina/gocsv v0.0.0-20230616125104-99d496ca653d h1:KbPOUXFUDJxwZ04vbmDOc3yuruGvVO+LOa7cVER3yWw=
+github.com/gocarina/gocsv v0.0.0-20230616125104-99d496ca653d/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI=
 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=
@@ -37,6 +39,7 @@ golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7w
 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 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 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=
 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=

+ 36 - 4
models/participant.go

@@ -1,12 +1,44 @@
 package models
 
+import (
+	"crypto/sha256"
+	"fmt"
+	"strings"
+)
+
 type Participant struct {
-	ID string `gorm:"primaryKey"`
+	ID string `csv:"id" gorm:"primaryKey"`
 
-	Firstname string
-	Lastname  string
+	Firstname string `csv:"firstname"`
+	Lastname  string `csv:"lastname"`
 
-	Token uint
+	Token uint `csv:"token"`
 
 	Attributes map[string]string
 }
+
+func (p *Participant) String() string {
+	return fmt.Sprintf("%s %s", p.Lastname, p.Firstname)
+}
+
+func (p *Participant) GetID() string {
+	return p.ID
+}
+
+func (p *Participant) SetID(id string) {
+	p.ID = id
+}
+
+func (p *Participant) GetHash() string {
+	return fmt.Sprintf("%x", sha256.Sum256([]byte(strings.Join(append([]string{p.Lastname, p.Firstname}, p.AttributesToSlice()...), ""))))
+}
+
+func (p *Participant) AttributesToSlice() []string {
+	result := make([]string, len(p.Attributes)*2)
+
+	for k, v := range p.Attributes {
+		result = append(result, k, v)
+	}
+
+	return result
+}

+ 5 - 0
store/collection.go

@@ -0,0 +1,5 @@
+package store
+
+import "git.andreafazzi.eu/andrea/probo/models"
+
+type CollectionStore = Store[*models.Collection]

+ 42 - 41
store/file/collection.go

@@ -9,45 +9,46 @@ import (
 	"git.andreafazzi.eu/andrea/probo/store"
 )
 
-func NewCollectionFileStore() (*FileStore[*models.Collection, *store.Store[*models.Collection]], error) {
-	return NewFileStore[*models.Collection](
-		store.NewStore[*models.Collection](),
-		filepath.Join(BaseDir, CollectionsDir),
-		"collection",
-		".json",
-		func(s *store.Store[*models.Collection], filepath string, content []byte) (*models.Collection, error) {
-			collection := new(models.Collection)
-			err := json.Unmarshal(content, &collection)
-			if err != nil {
-				return nil, err
-			}
-
-			c, err := s.Create(collection)
-			if err != nil {
-				return nil, err
-			}
-
-			return c, nil
-		},
-		func(s *store.Store[*models.Collection], filePath string, collection *models.Collection) error {
-			jsonData, err := json.Marshal(collection)
-			if err != nil {
-				return err
-			}
-
-			file, err := os.Create(filePath)
-			if err != nil {
-				return err
-			}
-
-			defer file.Close()
-
-			_, err = file.Write(jsonData)
-			if err != nil {
-				return err
-			}
-
-			return nil
-		},
-	)
+type CollectionFileStore = FileStore[*models.Collection, *store.Store[*models.Collection]]
+
+var DefaultCollectionDir = filepath.Join(BaseDir, CollectionsDir)
+
+func NewCollectionFileStore(config *FileStoreConfig[*models.Collection, *store.CollectionStore]) (*CollectionFileStore, error) {
+	return NewFileStore[*models.Collection](config, store.NewStore[*models.Collection]())
+}
+
+func DefaultUnmarshalCollectionFunc(s *store.Store[*models.Collection], filepath string, content []byte) (*models.Collection, error) {
+	collection := new(models.Collection)
+	err := json.Unmarshal(content, &collection)
+	if err != nil {
+		return nil, err
+	}
+
+	c, err := s.Create(collection)
+	if err != nil {
+		return nil, err
+	}
+
+	return c, nil
+}
+
+func DefaultMarshalCollectionFunc(s *store.Store[*models.Collection], filePath string, collection *models.Collection) error {
+	jsonData, err := json.Marshal(collection)
+	if err != nil {
+		return err
+	}
+
+	file, err := os.Create(filePath)
+	if err != nil {
+		return err
+	}
+
+	defer file.Close()
+
+	_, err = file.Write(jsonData)
+	if err != nil {
+		return err
+	}
+
+	return nil
 }

+ 8 - 1
store/file/collection_test.go

@@ -48,7 +48,14 @@ func (t *collectionTestSuite) TestCreateCollection() {
 			},
 		})
 
-	store, err := NewCollectionFileStore()
+	store, err := NewCollectionFileStore(
+		&FileStoreConfig[*models.Collection, *store.CollectionStore]{
+			FilePathConfig: FilePathConfig{"testdata", "collection", ".json"},
+			IndexDirFunc:   DefaultIndexDirFunc[*models.Collection, *store.CollectionStore],
+			UnmarshalFunc:  DefaultUnmarshalCollectionFunc,
+			MarshalFunc:    DefaultMarshalCollectionFunc,
+		},
+	)
 	t.Nil(err)
 
 	c := new(models.Collection)

+ 108 - 63
store/file/file.go

@@ -1,6 +1,7 @@
 package file
 
 import (
+	"encoding/json"
 	"errors"
 	"fmt"
 	"io/fs"
@@ -12,49 +13,121 @@ import (
 	"git.andreafazzi.eu/andrea/probo/store"
 )
 
+type IndexDirFunc[T store.Storable, K Storer[T]] func(s *FileStore[T, K]) error
+
 var (
 	ErrorMetaHeaderIsNotPresent = errors.New("Meta header was not found in file.")
 
-	BaseDir        = "data"
-	QuizzesDir     = "quizzes"
-	CollectionsDir = "collections"
+	BaseDir         = "data"
+	QuizzesDir      = "quizzes"
+	CollectionsDir  = "collections"
+	ParticipantsDir = "participants"
 )
 
 type Storer[T store.Storable] interface {
 	store.Storer[T]
-	// store.FilterStorer[T]
 }
 
-type FileStore[T store.Storable, K Storer[T]] struct {
-	Storer K
-
+type FilePathConfig struct {
 	Dir        string
 	FilePrefix string
 	FileSuffix string
+}
 
-	MarshalFunc   func(K, string, []byte) (T, error)
-	UnmarshalFunc func(K, string, T) error
+type FileStoreConfig[T store.Storable, K Storer[T]] struct {
+	FilePathConfig
+	FilepathFunc  func(T, *FilePathConfig) string
+	UnmarshalFunc func(K, string, []byte) (T, error)
+	MarshalFunc   func(K, string, T) error
+	IndexDirFunc  func(*FileStore[T, K]) error
+}
+
+type FileStore[T store.Storable, K Storer[T]] struct {
+	*FileStoreConfig[T, K]
+
+	Storer K
 
 	lock  sync.RWMutex
 	paths map[string]string
 }
 
-func NewFileStore[T store.Storable, K Storer[T]](
-	storer K,
-	dir string,
-	prefix string,
-	suffix string,
-	marshalFunc func(K, string, []byte) (T, error),
-	unmarshalFunc func(K, string, T) error,
-) (*FileStore[T, K], error) {
+func DefaultJSONUnmarshalFunc[T store.Storable, K Storer[T]](s K, filepath string, content []byte) (T, error) {
+	entity := new(T)
+	err := json.Unmarshal(content, &entity)
+	if err != nil {
+		return *entity, err
+	}
+
+	c, err := s.Create(*entity)
+	if err != nil {
+		return c, err
+	}
+
+	return c, nil
+}
+
+func DefaultJSONMarshalFunc[T store.Storable, K Storer[T]](s K, filePath string, entity T) error {
+	jsonData, err := json.Marshal(entity)
+	if err != nil {
+		return err
+	}
+
+	file, err := os.Create(filePath)
+	if err != nil {
+		return err
+	}
+
+	defer file.Close()
+
+	_, err = file.Write(jsonData)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func DefaultIndexDirFunc[T store.Storable, K Storer[T]](s *FileStore[T, K]) error {
+	files, err := os.ReadDir(s.Dir)
+	if err != nil {
+		return err
+	}
+
+	entityFiles := make([]fs.DirEntry, 0)
+
+	for _, file := range files {
+		filename := file.Name()
+		if !file.IsDir() && strings.HasSuffix(filename, s.FileSuffix) {
+			entityFiles = append(entityFiles, file)
+		}
+	}
+
+	for _, file := range entityFiles {
+		filename := file.Name()
+		fullPath := filepath.Join(s.Dir, filename)
+
+		content, err := os.ReadFile(fullPath)
+		if err != nil {
+			return err
+		}
+
+		entity, err := s.UnmarshalFunc(s.Storer, fullPath, content)
+		if err != nil {
+			return err
+		}
+
+		s.SetPath(entity, fullPath)
+	}
+
+	return nil
+
+}
+
+func NewFileStore[T store.Storable, K Storer[T]](config *FileStoreConfig[T, K], storer K) (*FileStore[T, K], error) {
 	store := &FileStore[T, K]{
-		Storer:        storer,
-		Dir:           dir,
-		FilePrefix:    prefix,
-		FileSuffix:    suffix,
-		MarshalFunc:   marshalFunc,
-		UnmarshalFunc: unmarshalFunc,
-		paths:         make(map[string]string, 0),
+		FileStoreConfig: config,
+		Storer:          storer,
+		paths:           make(map[string]string, 0),
 	}
 
 	err := store.IndexDir()
@@ -72,9 +145,15 @@ func (s *FileStore[T, K]) Create(entity T) (T, error) {
 		return e, err
 	}
 
-	filePath := filepath.Join(s.Dir, fmt.Sprintf("%s_%v%s", s.FilePrefix, e.GetID(), s.FileSuffix))
+	var filePath string
 
-	err = s.UnmarshalFunc(s.Storer, filePath, e)
+	if s.FilepathFunc == nil {
+		filePath = filepath.Join(s.Dir, fmt.Sprintf("%s_%v%s", s.FilePrefix, e.GetID(), s.FileSuffix))
+	} else {
+		filePath = s.FilepathFunc(entity, &s.FilePathConfig)
+	}
+
+	err = s.MarshalFunc(s.Storer, filePath, e)
 	if err != nil {
 		return e, err
 	}
@@ -90,15 +169,13 @@ func (s *FileStore[T, K]) Update(entity T, id string) (T, error) {
 		return e, err
 	}
 
-	filePath := filepath.Join(s.Dir, fmt.Sprintf("%s_%v%s", s.FilePrefix, e.GetID(), s.FileSuffix))
+	filePath := s.GetPath(e)
 
-	err = s.UnmarshalFunc(s.Storer, filePath, e)
+	err = s.MarshalFunc(s.Storer, filePath, e)
 	if err != nil {
 		return e, err
 	}
 
-	s.SetPath(e, filePath)
-
 	return e, nil
 }
 
@@ -125,39 +202,7 @@ func (s *FileStore[T, K]) Delete(id string) (T, error) {
 }
 
 func (s *FileStore[T, K]) IndexDir() error {
-	files, err := os.ReadDir(s.Dir)
-	if err != nil {
-		return err
-	}
-
-	entityFiles := make([]fs.DirEntry, 0)
-
-	for _, file := range files {
-		filename := file.Name()
-		if !file.IsDir() && strings.HasSuffix(filename, s.FileSuffix) {
-			entityFiles = append(entityFiles, file)
-		}
-	}
-
-	for _, file := range entityFiles {
-		filename := file.Name()
-		fullPath := filepath.Join(s.Dir, filename)
-
-		content, err := os.ReadFile(fullPath)
-		if err != nil {
-			return err
-		}
-
-		entity, err := s.MarshalFunc(s.Storer, fullPath, content)
-		if err != nil {
-			return err
-		}
-
-		s.SetPath(entity, fullPath)
-	}
-
-	return nil
-
+	return s.IndexDirFunc(s)
 }
 
 func (s *FileStore[T, K]) GetPath(entity T) string {

+ 1 - 0
store/file/file_test.go

@@ -13,5 +13,6 @@ func TestRunner(t *testing.T) {
 		t,
 		new(quizTestSuite),
 		new(collectionTestSuite),
+		new(participantTestSuite),
 	)
 }

+ 51 - 0
store/file/participant.go

@@ -0,0 +1,51 @@
+package file
+
+import (
+	"encoding/json"
+	"os"
+
+	"git.andreafazzi.eu/andrea/probo/models"
+	"git.andreafazzi.eu/andrea/probo/store"
+)
+
+type ParticipantFileStore = FileStore[*models.Participant, *store.Store[*models.Participant]]
+
+func NewParticipantFileStore(config *FileStoreConfig[*models.Participant, *store.Store[*models.Participant]]) (*ParticipantFileStore, error) {
+	return NewFileStore[*models.Participant](config, store.NewStore[*models.Participant]())
+}
+
+func DefaultUnmarshalParticipantFunc(s *store.Store[*models.Participant], filepath string, content []byte) (*models.Participant, error) {
+	participant := new(models.Participant)
+	err := json.Unmarshal(content, &participant)
+	if err != nil {
+		return nil, err
+	}
+
+	c, err := s.Create(participant)
+	if err != nil {
+		return nil, err
+	}
+
+	return c, nil
+}
+
+func DefaultMarshalParticipantFunc(s *store.Store[*models.Participant], filePath string, participant *models.Participant) error {
+	jsonData, err := json.Marshal(participant)
+	if err != nil {
+		return err
+	}
+
+	file, err := os.Create(filePath)
+	if err != nil {
+		return err
+	}
+
+	defer file.Close()
+
+	_, err = file.Write(jsonData)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}

+ 44 - 0
store/file/participant_test.go

@@ -0,0 +1,44 @@
+package file
+
+import (
+	"os"
+
+	"git.andreafazzi.eu/andrea/probo/models"
+	"git.andreafazzi.eu/andrea/probo/store"
+	"github.com/remogatto/prettytest"
+)
+
+type participantTestSuite struct {
+	prettytest.Suite
+}
+
+func (t *participantTestSuite) TestCreate() {
+	filePathConfig := FilePathConfig{"testdata/participants", "participant", ".json"}
+	pStore, err := NewParticipantFileStore(
+		&FileStoreConfig[*models.Participant, *store.ParticipantStore]{
+			FilePathConfig: filePathConfig,
+			IndexDirFunc:   DefaultIndexDirFunc[*models.Participant, *store.ParticipantStore],
+			UnmarshalFunc:  DefaultJSONUnmarshalFunc[*models.Participant, *store.ParticipantStore],
+			MarshalFunc:    DefaultJSONMarshalFunc[*models.Participant, *store.ParticipantStore],
+		})
+	t.Nil(err)
+
+	if !t.Failed() {
+		p, err := pStore.Create(&models.Participant{
+			Lastname:   "Doe",
+			Firstname:  "John",
+			Attributes: map[string]string{"class": "1 D LIN"},
+		})
+
+		t.Nil(err)
+
+		defer os.Remove(pStore.GetPath(p))
+
+		if !t.Failed() {
+			exists, err := os.Stat(pStore.GetPath(p))
+
+			t.Nil(err)
+			t.Not(t.Nil(exists))
+		}
+	}
+}

+ 98 - 46
store/file/quiz.go

@@ -6,7 +6,6 @@ import (
 	"errors"
 	"io"
 	"os"
-	"path/filepath"
 	"strings"
 	"time"
 
@@ -15,60 +14,59 @@ import (
 	"gopkg.in/yaml.v2"
 )
 
-func NewQuizFileStore() (*FileStore[*models.Quiz, *store.QuizStore], error) {
-	return NewFileStore[*models.Quiz, *store.QuizStore](
-		store.NewQuizStore(),
-		filepath.Join(BaseDir, QuizzesDir),
-		"quiz",
-		".md",
-		func(s *store.QuizStore, filepath string, content []byte) (*models.Quiz, error) {
-			quiz, meta, err := models.MarkdownToQuiz(string(content))
-			if err != nil {
-				return nil, err
-			}
+type QuizFileStore = FileStore[*models.Quiz, *store.QuizStore]
 
-			var errQuizAlreadyPresent *store.ErrQuizAlreadyPresent
+func NewQuizFileStore(config *FileStoreConfig[*models.Quiz, *store.QuizStore]) (*QuizFileStore, error) {
+	return NewFileStore[*models.Quiz, *store.QuizStore](config, store.NewQuizStore())
+}
 
-			q, err := s.Create(quiz)
-			if err != nil && !errors.As(err, &errQuizAlreadyPresent) {
-				return nil, err
-			}
+func DefaultUnmarshalQuizFunc(s *store.QuizStore, filepath string, content []byte) (*models.Quiz, error) {
+	quiz, meta, err := models.MarkdownToQuiz(string(content))
+	if err != nil {
+		return nil, err
+	}
 
-			if meta == nil {
-				writeQuizHeader(filepath, &models.Meta{
-					ID:        q.ID,
-					CreatedAt: time.Now(),
-				})
-			}
+	var errQuizAlreadyPresent *store.ErrQuizAlreadyPresent
 
-			return q, nil
-		},
-		func(s *store.QuizStore, filePath string, quiz *models.Quiz) error {
-			markdown, err := models.QuizToMarkdown(quiz)
-			if err != nil {
-				return err
-			}
+	q, err := s.Create(quiz)
+	if err != nil && !errors.As(err, &errQuizAlreadyPresent) {
+		return nil, err
+	}
 
-			file, err := os.Create(filePath)
-			if err != nil {
-				return err
-			}
+	if meta == nil {
+		writeQuizHeader(filepath, &models.Meta{
+			ID:        q.ID,
+			CreatedAt: time.Now(),
+		})
+	}
 
-			defer file.Close()
+	return q, nil
+}
 
-			markdownWithMetaHeader, err := addMetaHeaderToMarkdown(markdown, &quiz.Meta)
-			if err != nil {
-				return err
-			}
+func DefaultMarshalQuizFunc(s *store.QuizStore, filePath string, quiz *models.Quiz) error {
+	markdown, err := models.QuizToMarkdown(quiz)
+	if err != nil {
+		return err
+	}
 
-			_, err = file.Write([]byte(markdownWithMetaHeader))
-			if err != nil {
-				return err
-			}
+	file, err := os.Create(filePath)
+	if err != nil {
+		return err
+	}
+
+	defer file.Close()
 
-			return nil
-		},
-	)
+	markdownWithMetaHeader, err := addMetaHeaderToMarkdown(markdown, &quiz.Meta)
+	if err != nil {
+		return err
+	}
+
+	_, err = file.Write([]byte(markdownWithMetaHeader))
+	if err != nil {
+		return err
+	}
+
+	return nil
 }
 
 func writeQuizHeader(path string, meta *models.Meta) (*models.Meta, error) {
@@ -214,3 +212,57 @@ func removeQuizHeader(path string) (*models.Meta, error) {
 
 	return &meta, nil
 }
+
+// 		filepath.Join(BaseDir, QuizzesDir),
+// 	"quiz",
+// 	".md",
+// 	DefaultIndexDirFunc,
+// 	nil,
+// 	func(s *store.QuizStore, filepath string, content []byte) (*models.Quiz, error) {
+// 		quiz, meta, err := models.MarkdownToQuiz(string(content))
+// 		if err != nil {
+// 			return nil, err
+// 		}
+
+// 		var errQuizAlreadyPresent *store.ErrQuizAlreadyPresent
+
+// 		q, err := s.Create(quiz)
+// 		if err != nil && !errors.As(err, &errQuizAlreadyPresent) {
+// 			return nil, err
+// 		}
+
+// 		if meta == nil {
+// 			writeQuizHeader(filepath, &models.Meta{
+// 				ID:        q.ID,
+// 				CreatedAt: time.Now(),
+// 			})
+// 		}
+
+// 		return q, nil
+// 	},
+// 	func(s *store.QuizStore, filePath string, quiz *models.Quiz) error {
+// 		markdown, err := models.QuizToMarkdown(quiz)
+// 		if err != nil {
+// 			return err
+// 		}
+
+// 		file, err := os.Create(filePath)
+// 		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
+// 		}
+
+// 		return nil
+// 	},
+// )

+ 46 - 9
store/file/quiz_test.go

@@ -6,6 +6,7 @@ import (
 	"path/filepath"
 
 	"git.andreafazzi.eu/andrea/probo/models"
+	"git.andreafazzi.eu/andrea/probo/store"
 	"github.com/remogatto/prettytest"
 )
 
@@ -13,12 +14,16 @@ type quizTestSuite struct {
 	prettytest.Suite
 }
 
-func (t *quizTestSuite) BeforeAll() {
-	BaseDir = "testdata"
-}
-
 func (t *quizTestSuite) TestReadAll() {
-	store, err := NewQuizFileStore()
+	filePathConfig := FilePathConfig{"testdata/quizzes", "quiz", ".md"}
+	store, err := NewQuizFileStore(
+		&FileStoreConfig[*models.Quiz, *store.QuizStore]{
+			FilePathConfig: filePathConfig,
+			IndexDirFunc:   DefaultIndexDirFunc[*models.Quiz, *store.QuizStore],
+			UnmarshalFunc:  DefaultUnmarshalQuizFunc,
+			MarshalFunc:    DefaultMarshalQuizFunc,
+		},
+	)
 	t.Nil(err)
 
 	if !t.Failed() {
@@ -36,7 +41,15 @@ func (t *quizTestSuite) TestReadAll() {
 }
 
 func (t *quizTestSuite) TestCreate() {
-	store, err := NewQuizFileStore()
+	filePathConfig := FilePathConfig{"testdata/quizzes", "quiz", ".md"}
+	store, err := NewQuizFileStore(
+		&FileStoreConfig[*models.Quiz, *store.QuizStore]{
+			FilePathConfig: filePathConfig,
+			IndexDirFunc:   DefaultIndexDirFunc[*models.Quiz, *store.QuizStore],
+			UnmarshalFunc:  DefaultUnmarshalQuizFunc,
+			MarshalFunc:    DefaultMarshalQuizFunc,
+		},
+	)
 	t.Nil(err)
 
 	if !t.Failed() {
@@ -90,7 +103,15 @@ func (t *quizTestSuite) TestCreate() {
 }
 
 func (t *quizTestSuite) TestDelete() {
-	store, err := NewQuizFileStore()
+	filePathConfig := FilePathConfig{"testdata/quizzes", "quiz", ".md"}
+	store, err := NewQuizFileStore(
+		&FileStoreConfig[*models.Quiz, *store.QuizStore]{
+			FilePathConfig: filePathConfig,
+			IndexDirFunc:   DefaultIndexDirFunc[*models.Quiz, *store.QuizStore],
+			UnmarshalFunc:  DefaultUnmarshalQuizFunc,
+			MarshalFunc:    DefaultMarshalQuizFunc,
+		},
+	)
 	t.Nil(err)
 
 	if !t.Failed() {
@@ -121,7 +142,15 @@ func (t *quizTestSuite) TestDelete() {
 }
 
 func (t *quizTestSuite) TestUpdate() {
-	store, err := NewQuizFileStore()
+	filePathConfig := FilePathConfig{"testdata/quizzes", "quiz", ".md"}
+	store, err := NewQuizFileStore(
+		&FileStoreConfig[*models.Quiz, *store.QuizStore]{
+			FilePathConfig: filePathConfig,
+			IndexDirFunc:   DefaultIndexDirFunc[*models.Quiz, *store.QuizStore],
+			UnmarshalFunc:  DefaultUnmarshalQuizFunc,
+			MarshalFunc:    DefaultMarshalQuizFunc,
+		},
+	)
 	t.Nil(err)
 
 	if !t.Failed() {
@@ -161,7 +190,15 @@ func (t *quizTestSuite) TestUpdate() {
 }
 
 func (t *quizTestSuite) TestAutowriteHeader() {
-	store, err := NewQuizFileStore()
+	filePathConfig := FilePathConfig{"testdata/quizzes", "quiz", ".md"}
+	store, err := NewQuizFileStore(
+		&FileStoreConfig[*models.Quiz, *store.QuizStore]{
+			FilePathConfig: filePathConfig,
+			IndexDirFunc:   DefaultIndexDirFunc[*models.Quiz, *store.QuizStore],
+			UnmarshalFunc:  DefaultUnmarshalQuizFunc,
+			MarshalFunc:    DefaultMarshalQuizFunc,
+		},
+	)
 	t.Nil(err)
 
 	if !t.Failed() {

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

@@ -1,6 +1,6 @@
 ---
-id: 57ebde24-51a0-421e-a641-4a5c198473c3
-created_at: 2023-11-13T21:02:55.786449849+01:00
+id: edadaa90-802f-4a74-83cc-4b4cb5192f28
+created_at: 2023-11-17T16:50:38.479350601+01:00
 updated_at: 0001-01-01T00:00:00Z
 ---
 This quiz is initially without metadata.

+ 5 - 0
store/participant.go

@@ -0,0 +1,5 @@
+package store
+
+import "git.andreafazzi.eu/andrea/probo/models"
+
+type ParticipantStore = Store[*models.Participant]

+ 52 - 0
store/participant_test.go

@@ -0,0 +1,52 @@
+package store
+
+import (
+	"git.andreafazzi.eu/andrea/probo/models"
+	"github.com/remogatto/prettytest"
+)
+
+type participantTestSuite struct {
+	prettytest.Suite
+}
+
+func (t *participantTestSuite) TestCreate() {
+	store := NewStore[*models.Participant]()
+
+	p_1, err := store.Create(&models.Participant{
+		Lastname:  "Doe",
+		Firstname: "John",
+	})
+
+	t.Nil(err)
+
+	p_2, err := store.Create(&models.Participant{
+		Lastname:   "Doe",
+		Firstname:  "John",
+		Attributes: map[string]string{"class": "1 D LIN"},
+	})
+
+	t.Nil(err)
+
+	t.False(p_1.GetHash() == p_2.GetHash())
+}
+
+func (t *participantTestSuite) TestUpdate() {
+	store := NewStore[*models.Participant]()
+
+	p, err := store.Create(&models.Participant{
+		Lastname:  "Doe",
+		Firstname: "John",
+	})
+
+	t.Nil(err)
+
+	updatedP, err := store.Update(&models.Participant{
+		Lastname:   "Doe",
+		Firstname:  "John",
+		Attributes: map[string]string{"class": "1 D LIN"},
+	}, p.ID)
+
+	t.Nil(err)
+
+	t.False(p.GetHash() == updatedP.GetHash())
+}

+ 1 - 0
store/store_test.go

@@ -11,5 +11,6 @@ func TestRunner(t *testing.T) {
 		t,
 		new(quizTestSuite),
 		new(collectionTestSuite),
+		new(participantTestSuite),
 	)
 }