From 7060c54989f2d7501528ef87002554bb1f0b8a43 Mon Sep 17 00:00:00 2001 From: Niko Abeler Date: Sun, 25 Jun 2023 20:04:06 +0200 Subject: [PATCH] init --- .gitignore | 26 +++++ app/repository/interfaces.go | 12 +++ domain/model/entry.go | 16 ++++ domain/model/image_entry.go | 40 ++++++++ go.mod | 13 +++ go.sum | 24 +++++ infra/entry_repository.go | 168 +++++++++++++++++++++++++++++++++ infra/entry_repository_test.go | 144 ++++++++++++++++++++++++++++ infra/interface.go | 7 ++ main.go | 30 ++++++ test/mock_db.go | 19 ++++ test/mock_entry.go | 43 +++++++++ 12 files changed, 542 insertions(+) create mode 100644 .gitignore create mode 100644 app/repository/interfaces.go create mode 100644 domain/model/entry.go create mode 100644 domain/model/image_entry.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 infra/entry_repository.go create mode 100644 infra/entry_repository_test.go create mode 100644 infra/interface.go create mode 100644 main.go create mode 100644 test/mock_db.go create mode 100644 test/mock_entry.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e610cac --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work + +users/ + +.vscode/ +*.swp diff --git a/app/repository/interfaces.go b/app/repository/interfaces.go new file mode 100644 index 0000000..3f97dca --- /dev/null +++ b/app/repository/interfaces.go @@ -0,0 +1,12 @@ +package repository + +import "owl-blogs/domain/model" + +type EntryRepository interface { + RegisterEntryType(entry model.Entry) error + Create(entry model.Entry) error + Update(entry model.Entry) error + Delete(entry model.Entry) error + FindById(id string) (model.Entry, error) + FindAll(types *[]string) ([]model.Entry, error) +} diff --git a/domain/model/entry.go b/domain/model/entry.go new file mode 100644 index 0000000..0b119de --- /dev/null +++ b/domain/model/entry.go @@ -0,0 +1,16 @@ +package model + +import "time" + +type EntryContent string + +type Entry interface { + ID() string + Content() EntryContent + PublishedAt() *time.Time + MetaData() interface{} + Create(id string, content string, publishedAt *time.Time, metaData EntryMetaData) error +} + +type EntryMetaData interface { +} diff --git a/domain/model/image_entry.go b/domain/model/image_entry.go new file mode 100644 index 0000000..a60ea0c --- /dev/null +++ b/domain/model/image_entry.go @@ -0,0 +1,40 @@ +package model + +import "time" + +type ImageEntry struct { + id string + content EntryContent + publishedAt *time.Time + ImagePath string +} + +type ImageEntryMetaData struct { + ImagePath string +} + +func (e *ImageEntry) ID() string { + return e.id +} + +func (e *ImageEntry) Content() EntryContent { + return e.content +} + +func (e *ImageEntry) PublishedAt() *time.Time { + return e.publishedAt +} + +func (e *ImageEntry) MetaData() interface{} { + return &ImageEntryMetaData{ + ImagePath: e.ImagePath, + } +} + +func (e *ImageEntry) Create(id string, content string, publishedAt *time.Time, metaData EntryMetaData) error { + e.id = id + e.content = EntryContent(content) + e.publishedAt = publishedAt + e.ImagePath = metaData.(*ImageEntryMetaData).ImagePath + return nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..67a03f1 --- /dev/null +++ b/go.mod @@ -0,0 +1,13 @@ +module owl-blogs + +go 1.20 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/jmoiron/sqlx v1.3.5 // indirect + github.com/mattn/go-sqlite3 v1.14.17 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/objx v0.5.0 // indirect + github.com/stretchr/testify v1.8.4 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..2c70d96 --- /dev/null +++ b/go.sum @@ -0,0 +1,24 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= +github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= +github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/infra/entry_repository.go b/infra/entry_repository.go new file mode 100644 index 0000000..f347347 --- /dev/null +++ b/infra/entry_repository.go @@ -0,0 +1,168 @@ +package infra + +import ( + "encoding/json" + "errors" + "owl-blogs/app/repository" + "owl-blogs/domain/model" + "reflect" + "strings" + "time" + + "github.com/jmoiron/sqlx" + _ "github.com/mattn/go-sqlite3" +) + +type sqlEntry struct { + Id string `db:"id"` + Type string `db:"type"` + Content string `db:"content"` + PublishedAt *time.Time `db:"published_at"` + MetaData *string `db:"meta_data"` +} + +type DefaultEntryRepo struct { + types map[string]model.Entry + db *sqlx.DB +} + +// Create implements repository.EntryRepository. +func (r *DefaultEntryRepo) Create(entry model.Entry) error { + exEntry, _ := r.FindById(entry.ID()) + if exEntry != nil { + return errors.New("entry already exists") + } + + t := r.entryType(entry) + if _, ok := r.types[t]; !ok { + return errors.New("entry type not registered") + } + + var metaDataJson []byte + if entry.MetaData() != nil { + metaDataJson, _ = json.Marshal(entry.MetaData()) + } + + _, err := r.db.Exec("INSERT INTO entries (id, type, content, published_at, meta_data) VALUES (?, ?, ?, ?, ?)", entry.ID(), t, entry.Content(), entry.PublishedAt(), metaDataJson) + return err +} + +// Delete implements repository.EntryRepository. +func (r *DefaultEntryRepo) Delete(entry model.Entry) error { + _, err := r.db.Exec("DELETE FROM entries WHERE id = ?", entry.ID()) + return err +} + +// FindAll implements repository.EntryRepository. +func (r *DefaultEntryRepo) FindAll(types *[]string) ([]model.Entry, error) { + filterStr := "" + if types != nil { + filters := []string{} + for _, t := range *types { + filters = append(filters, "type = '"+t+"'") + } + filterStr = strings.Join(filters, " OR ") + } + + var entries []sqlEntry + if filterStr != "" { + err := r.db.Select(&entries, "SELECT * FROM entries WHERE "+filterStr) + if err != nil { + return nil, err + } + } else { + err := r.db.Select(&entries, "SELECT * FROM entries") + if err != nil { + return nil, err + } + } + + result := []model.Entry{} + for _, entry := range entries { + e, err := r.sqlEntryToEntry(entry) + if err != nil { + return nil, err + } + result = append(result, e) + } + return result, nil +} + +// FindById implements repository.EntryRepository. +func (r *DefaultEntryRepo) FindById(id string) (model.Entry, error) { + data := sqlEntry{} + err := r.db.Get(&data, "SELECT * FROM entries WHERE id = ?", id) + if err != nil { + return nil, err + } + if data.Id == "" { + return nil, nil + } + return r.sqlEntryToEntry(data) +} + +// RegisterEntryType implements repository.EntryRepository. +func (r *DefaultEntryRepo) RegisterEntryType(entry model.Entry) error { + t := r.entryType(entry) + if _, ok := r.types[t]; ok { + return errors.New("entry type already registered") + } + r.types[t] = entry + return nil +} + +// Update implements repository.EntryRepository. +func (r *DefaultEntryRepo) Update(entry model.Entry) error { + exEntry, _ := r.FindById(entry.ID()) + if exEntry == nil { + return errors.New("entry not found") + } + + t := r.entryType(entry) + if _, ok := r.types[t]; !ok { + return errors.New("entry type not registered") + } + + var metaDataJson []byte + if entry.MetaData() != nil { + metaDataJson, _ = json.Marshal(entry.MetaData()) + } + + _, err := r.db.Exec("UPDATE entries SET content = ?, published_at = ?, meta_data = ? WHERE id = ?", entry.Content(), entry.PublishedAt(), metaDataJson, entry.ID()) + return err +} + +func NewEntryRepository(db Database) repository.EntryRepository { + sqlxdb := db.Get() + + // Create tables if not exists + sqlxdb.MustExec(` + CREATE TABLE IF NOT EXISTS entries ( + id TEXT PRIMARY KEY, + type TEXT NOT NULL, + content TEXT NOT NULL, + published_at DATETIME, + meta_data TEXT NOT NULL + ); + `) + + return &DefaultEntryRepo{ + types: map[string]model.Entry{}, + db: sqlxdb, + } +} + +func (r *DefaultEntryRepo) entryType(entry model.Entry) string { + return reflect.TypeOf(entry).Elem().Name() +} + +func (r *DefaultEntryRepo) sqlEntryToEntry(entry sqlEntry) (model.Entry, error) { + e, ok := r.types[entry.Type] + if !ok { + return nil, errors.New("entry type not registered") + } + metaData := reflect.New(reflect.TypeOf(e.MetaData()).Elem()).Interface() + json.Unmarshal([]byte(*entry.MetaData), metaData) + e.Create(entry.Id, entry.Content, entry.PublishedAt, metaData) + return e, nil +} diff --git a/infra/entry_repository_test.go b/infra/entry_repository_test.go new file mode 100644 index 0000000..95a1373 --- /dev/null +++ b/infra/entry_repository_test.go @@ -0,0 +1,144 @@ +package infra_test + +import ( + "owl-blogs/app/repository" + "owl-blogs/infra" + "owl-blogs/test" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func setupRepo() repository.EntryRepository { + db := test.NewMockDb() + repo := infra.NewEntryRepository(db) + repo.RegisterEntryType(&test.MockEntry{}) + return repo +} + +func TestRepoRegister(t *testing.T) { + db := test.NewMockDb() + repo := infra.NewEntryRepository(db) + err := repo.RegisterEntryType(&test.MockEntry{}) + require.NoError(t, err) + + err = repo.RegisterEntryType(&test.MockEntry{}) + require.Error(t, err) +} + +func TestRepoCreate(t *testing.T) { + repo := setupRepo() + + entry := &test.MockEntry{} + now := time.Now() + entry.Create("id", "content", &now, &test.MockEntryMetaData{ + Str: "str", + Number: 1, + Date: now, + }) + err := repo.Create(entry) + require.NoError(t, err) + + entry2, err := repo.FindById("id") + require.NoError(t, err) + require.Equal(t, entry.ID(), entry2.ID()) + require.Equal(t, entry.Content(), entry2.Content()) + require.Equal(t, entry.PublishedAt().Unix(), entry2.PublishedAt().Unix()) + meta := entry.MetaData().(*test.MockEntryMetaData) + meta2 := entry2.MetaData().(*test.MockEntryMetaData) + require.Equal(t, meta.Str, meta2.Str) + require.Equal(t, meta.Number, meta2.Number) + require.Equal(t, meta.Date.Unix(), meta2.Date.Unix()) +} + +func TestRepoDelete(t *testing.T) { + repo := setupRepo() + + entry := &test.MockEntry{} + now := time.Now() + entry.Create("id", "content", &now, &test.MockEntryMetaData{ + Str: "str", + Number: 1, + Date: now, + }) + err := repo.Create(entry) + require.NoError(t, err) + + err = repo.Delete(entry) + require.NoError(t, err) + + _, err = repo.FindById("id") + require.Error(t, err) +} + +func TestRepoFindAll(t *testing.T) { + repo := setupRepo() + + entry := &test.MockEntry{} + now := time.Now() + entry.Create("id", "content", &now, &test.MockEntryMetaData{ + Str: "str", + Number: 1, + Date: now, + }) + err := repo.Create(entry) + require.NoError(t, err) + + entry2 := &test.MockEntry{} + now2 := time.Now() + entry2.Create("id2", "content2", &now2, &test.MockEntryMetaData{ + Str: "str2", + Number: 2, + Date: now2, + }) + err = repo.Create(entry2) + require.NoError(t, err) + + entries, err := repo.FindAll(nil) + require.NoError(t, err) + require.Equal(t, 2, len(entries)) + + entries, err = repo.FindAll(&[]string{"MockEntry"}) + require.NoError(t, err) + require.Equal(t, 2, len(entries)) + + entries, err = repo.FindAll(&[]string{"MockEntry2"}) + require.NoError(t, err) + require.Equal(t, 0, len(entries)) + +} + +func TestRepoUpdate(t *testing.T) { + repo := setupRepo() + + entry := &test.MockEntry{} + now := time.Now() + entry.Create("id", "content", &now, &test.MockEntryMetaData{ + Str: "str", + Number: 1, + Date: now, + }) + err := repo.Create(entry) + require.NoError(t, err) + + entry2 := &test.MockEntry{} + now2 := time.Now() + entry2.Create("id", "content2", &now2, &test.MockEntryMetaData{ + Str: "str2", + Number: 2, + Date: now2, + }) + err = repo.Update(entry2) + require.NoError(t, err) + + entry3, err := repo.FindById("id") + require.NoError(t, err) + require.Equal(t, entry3.Content(), entry2.Content()) + require.Equal(t, entry3.PublishedAt().Unix(), entry2.PublishedAt().Unix()) + meta := entry3.MetaData().(*test.MockEntryMetaData) + meta2 := entry2.MetaData().(*test.MockEntryMetaData) + require.Equal(t, meta.Str, meta2.Str) + require.Equal(t, meta.Number, meta2.Number) + require.Equal(t, meta.Date.Unix(), meta2.Date.Unix()) +} diff --git a/infra/interface.go b/infra/interface.go new file mode 100644 index 0000000..367f929 --- /dev/null +++ b/infra/interface.go @@ -0,0 +1,7 @@ +package infra + +import "github.com/jmoiron/sqlx" + +type Database interface { + Get() *sqlx.DB +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..13c39ec --- /dev/null +++ b/main.go @@ -0,0 +1,30 @@ +package main + +import ( + "fmt" + "owl-blogs/domain/model" + "reflect" +) + +func Persist(entry model.Entry) error { + t := reflect.TypeOf(entry).Elem().Name() + + fmt.Println(t) + return nil +} + +func main() { + // repo := infra.NewEntryRepository() + // repo.RegisterEntryType(&model.ImageEntry{}) + + // var img model.Entry = &model.ImageEntry{} + // img.Create("id", "content", nil, &model.ImageEntryMetaData{ImagePath: "path"}) + + // repo.Save(img) + + // img2, err := repo.FindById("id") + // if err != nil { + // panic(err) + // } + // fmt.Println(img2) +} diff --git a/test/mock_db.go b/test/mock_db.go new file mode 100644 index 0000000..ac1fc74 --- /dev/null +++ b/test/mock_db.go @@ -0,0 +1,19 @@ +package test + +import ( + "github.com/jmoiron/sqlx" + _ "github.com/mattn/go-sqlite3" +) + +type MockDb struct { + db *sqlx.DB +} + +func (d *MockDb) Get() *sqlx.DB { + return d.db +} + +func NewMockDb() *MockDb { + db := sqlx.MustOpen("sqlite3", ":memory:") + return &MockDb{db: db} +} diff --git a/test/mock_entry.go b/test/mock_entry.go new file mode 100644 index 0000000..c58742b --- /dev/null +++ b/test/mock_entry.go @@ -0,0 +1,43 @@ +package test + +import ( + "owl-blogs/domain/model" + "time" +) + +type MockEntryMetaData struct { + Str string + Number int + Date time.Time +} + +type MockEntry struct { + id string + content model.EntryContent + publishedAt *time.Time + metaData *MockEntryMetaData +} + +func (e *MockEntry) ID() string { + return e.id +} + +func (e *MockEntry) Content() model.EntryContent { + return e.content +} + +func (e *MockEntry) PublishedAt() *time.Time { + return e.publishedAt +} + +func (e *MockEntry) MetaData() interface{} { + return e.metaData +} + +func (e *MockEntry) Create(id string, content string, publishedAt *time.Time, metaData model.EntryMetaData) error { + e.id = id + e.content = model.EntryContent(content) + e.publishedAt = publishedAt + e.metaData = metaData.(*MockEntryMetaData) + return nil +}