Completed first refactoring with Go generics

This commit is contained in:
andrea 2023-11-18 11:12:07 +01:00
parent 8382cc4222
commit 3cdfa72403
16 changed files with 501 additions and 164 deletions

1
go.mod
View file

@ -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
go.sum
View file

@ -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=

View file

@ -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
store/collection.go Normal file
View file

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

View file

@ -9,13 +9,15 @@ 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) {
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 {
@ -28,8 +30,9 @@ func NewCollectionFileStore() (*FileStore[*models.Collection, *store.Store[*mode
}
return c, nil
},
func(s *store.Store[*models.Collection], filePath string, collection *models.Collection) error {
}
func DefaultMarshalCollectionFunc(s *store.Store[*models.Collection], filePath string, collection *models.Collection) error {
jsonData, err := json.Marshal(collection)
if err != nil {
return err
@ -48,6 +51,4 @@ func NewCollectionFileStore() (*FileStore[*models.Collection, *store.Store[*mode
}
return nil
},
)
}

View file

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

View file

@ -1,6 +1,7 @@
package file
import (
"encoding/json"
"errors"
"fmt"
"io/fs"
@ -12,48 +13,120 @@ 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"
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]{
FileStoreConfig: config,
Storer: storer,
Dir: dir,
FilePrefix: prefix,
FileSuffix: suffix,
MarshalFunc: marshalFunc,
UnmarshalFunc: unmarshalFunc,
paths: make(map[string]string, 0),
}
@ -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 {

View file

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

51
store/file/participant.go Normal file
View file

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

View file

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

View file

@ -6,7 +6,6 @@ import (
"errors"
"io"
"os"
"path/filepath"
"strings"
"time"
@ -15,13 +14,13 @@ 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) {
type QuizFileStore = FileStore[*models.Quiz, *store.QuizStore]
func NewQuizFileStore(config *FileStoreConfig[*models.Quiz, *store.QuizStore]) (*QuizFileStore, error) {
return NewFileStore[*models.Quiz, *store.QuizStore](config, store.NewQuizStore())
}
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
@ -42,8 +41,9 @@ func NewQuizFileStore() (*FileStore[*models.Quiz, *store.QuizStore], error) {
}
return q, nil
},
func(s *store.QuizStore, filePath string, quiz *models.Quiz) error {
}
func DefaultMarshalQuizFunc(s *store.QuizStore, filePath string, quiz *models.Quiz) error {
markdown, err := models.QuizToMarkdown(quiz)
if err != nil {
return err
@ -67,8 +67,6 @@ func NewQuizFileStore() (*FileStore[*models.Quiz, *store.QuizStore], error) {
}
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
// },
// )

View file

@ -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() {

View file

@ -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
store/participant.go Normal file
View file

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

52
store/participant_test.go Normal file
View file

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

View file

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