This commit is contained in:
Niko Abeler 2023-07-06 19:36:24 +02:00
parent bcacbf1e4d
commit ff193f62e9
7 changed files with 132 additions and 43 deletions

View File

@ -3,6 +3,7 @@ package app
import ( import (
"owl-blogs/app/repository" "owl-blogs/app/repository"
"owl-blogs/domain/model" "owl-blogs/domain/model"
"time"
) )
type EntryService struct { type EntryService struct {
@ -13,8 +14,8 @@ func NewEntryService(entryRepository repository.EntryRepository) *EntryService {
return &EntryService{EntryRepository: entryRepository} return &EntryService{EntryRepository: entryRepository}
} }
func (s *EntryService) Create(entry model.Entry) error { func (s *EntryService) Create(entry model.Entry, publishedAt *time.Time, metaData model.EntryMetaData) error {
return s.EntryRepository.Create(entry) return s.EntryRepository.Create(entry, publishedAt, metaData)
} }
func (s *EntryService) Update(entry model.Entry) error { func (s *EntryService) Update(entry model.Entry) error {

View File

@ -1,9 +1,12 @@
package repository package repository
import "owl-blogs/domain/model" import (
"owl-blogs/domain/model"
"time"
)
type EntryRepository interface { type EntryRepository interface {
Create(entry model.Entry) error Create(entry model.Entry, publishedAt *time.Time, metaData model.EntryMetaData) error
Update(entry model.Entry) error Update(entry model.Entry) error
Delete(entry model.Entry) error Delete(entry model.Entry) error
FindById(id string) (model.Entry, error) FindById(id string) (model.Entry, error)

View File

@ -10,6 +10,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/google/uuid"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
_ "github.com/mattn/go-sqlite3" _ "github.com/mattn/go-sqlite3"
) )
@ -27,12 +28,7 @@ type DefaultEntryRepo struct {
} }
// Create implements repository.EntryRepository. // Create implements repository.EntryRepository.
func (r *DefaultEntryRepo) Create(entry model.Entry) error { func (r *DefaultEntryRepo) Create(entry model.Entry, publishedAt *time.Time, metaData model.EntryMetaData) error {
exEntry, _ := r.FindById(entry.ID())
if exEntry != nil {
return errors.New("entry already exists")
}
t, err := r.typeRegistry.TypeName(entry) t, err := r.typeRegistry.TypeName(entry)
if err != nil { if err != nil {
return errors.New("entry type not registered") return errors.New("entry type not registered")
@ -40,10 +36,12 @@ func (r *DefaultEntryRepo) Create(entry model.Entry) error {
var metaDataJson []byte var metaDataJson []byte
if entry.MetaData() != nil { if entry.MetaData() != nil {
metaDataJson, _ = json.Marshal(entry.MetaData()) metaDataJson, _ = json.Marshal(metaData)
} }
_, err = r.db.Exec("INSERT INTO entries (id, type, published_at, meta_data) VALUES (?, ?, ?, ?)", entry.ID(), t, entry.PublishedAt(), metaDataJson) id := uuid.New().String()
_, err = r.db.Exec("INSERT INTO entries (id, type, published_at, meta_data) VALUES (?, ?, ?, ?)", id, t, publishedAt, metaDataJson)
entry.Create(id, publishedAt, metaData)
return err return err
} }

View File

@ -24,15 +24,14 @@ func TestRepoCreate(t *testing.T) {
entry := &test.MockEntry{} entry := &test.MockEntry{}
now := time.Now() now := time.Now()
entry.Create("id", &now, &test.MockEntryMetaData{ err := repo.Create(entry, &now, &test.MockEntryMetaData{
Str: "str", Str: "str",
Number: 1, Number: 1,
Date: now, Date: now,
}) })
err := repo.Create(entry)
require.NoError(t, err) require.NoError(t, err)
entry2, err := repo.FindById("id") entry2, err := repo.FindById(entry.ID())
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, entry.ID(), entry2.ID()) require.Equal(t, entry.ID(), entry2.ID())
require.Equal(t, entry.Content(), entry2.Content()) require.Equal(t, entry.Content(), entry2.Content())
@ -49,12 +48,11 @@ func TestRepoDelete(t *testing.T) {
entry := &test.MockEntry{} entry := &test.MockEntry{}
now := time.Now() now := time.Now()
entry.Create("id", &now, &test.MockEntryMetaData{ err := repo.Create(entry, &now, &test.MockEntryMetaData{
Str: "str", Str: "str",
Number: 1, Number: 1,
Date: now, Date: now,
}) })
err := repo.Create(entry)
require.NoError(t, err) require.NoError(t, err)
err = repo.Delete(entry) err = repo.Delete(entry)
@ -69,22 +67,21 @@ func TestRepoFindAll(t *testing.T) {
entry := &test.MockEntry{} entry := &test.MockEntry{}
now := time.Now() now := time.Now()
entry.Create("id", &now, &test.MockEntryMetaData{ err := repo.Create(entry, &now, &test.MockEntryMetaData{
Str: "str", Str: "str",
Number: 1, Number: 1,
Date: now, Date: now,
}) })
err := repo.Create(entry)
require.NoError(t, err) require.NoError(t, err)
entry2 := &test.MockEntry{} entry2 := &test.MockEntry{}
now2 := time.Now() now2 := time.Now()
entry2.Create("id2", &now2, &test.MockEntryMetaData{ err = repo.Create(entry2, &now2, &test.MockEntryMetaData{
Str: "str2", Str: "str",
Number: 2, Number: 1,
Date: now2, Date: now,
}) })
err = repo.Create(entry2)
require.NoError(t, err) require.NoError(t, err)
entries, err := repo.FindAll(nil) entries, err := repo.FindAll(nil)
@ -106,25 +103,25 @@ func TestRepoUpdate(t *testing.T) {
entry := &test.MockEntry{} entry := &test.MockEntry{}
now := time.Now() now := time.Now()
entry.Create("id", &now, &test.MockEntryMetaData{ err := repo.Create(entry, &now, &test.MockEntryMetaData{
Str: "str", Str: "str",
Number: 1, Number: 1,
Date: now, Date: now,
}) })
err := repo.Create(entry)
require.NoError(t, err) require.NoError(t, err)
entry2 := &test.MockEntry{} entry2 := &test.MockEntry{}
now2 := time.Now() now2 := time.Now()
entry2.Create("id", &now2, &test.MockEntryMetaData{ err = repo.Create(entry2, &now2, &test.MockEntryMetaData{
Str: "str2", Str: "str2",
Number: 2, Number: 2,
Date: now2, Date: now2,
}) })
require.NoError(t, err)
err = repo.Update(entry2) err = repo.Update(entry2)
require.NoError(t, err) require.NoError(t, err)
entry3, err := repo.FindById("id") entry3, err := repo.FindById(entry2.ID())
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, entry3.Content(), entry2.Content()) require.Equal(t, entry3.Content(), entry2.Content())
require.Equal(t, entry3.PublishedAt().Unix(), entry2.PublishedAt().Unix()) require.Equal(t, entry3.PublishedAt().Unix(), entry2.PublishedAt().Unix())

View File

@ -1,6 +1,7 @@
package editor_test package editor_test
import ( import (
"mime/multipart"
"owl-blogs/domain/model" "owl-blogs/domain/model"
"owl-blogs/web/editor" "owl-blogs/web/editor"
"reflect" "reflect"
@ -15,10 +16,21 @@ type MockEntryMetaData struct {
Content string `owl:"inputType=text"` Content string `owl:"inputType=text"`
} }
type MockFormData struct {
}
func (f *MockFormData) FormFile(key string) (*multipart.FileHeader, error) {
return nil, nil
}
func (f *MockFormData) FormValue(key string, defaultValue ...string) string {
return key
}
type MockEntry struct { type MockEntry struct {
id string id string
publishedAt *time.Time publishedAt *time.Time
metaData *MockEntryMetaData metaData MockEntryMetaData
} }
func (e *MockEntry) ID() string { func (e *MockEntry) ID() string {
@ -34,13 +46,13 @@ func (e *MockEntry) PublishedAt() *time.Time {
} }
func (e *MockEntry) MetaData() interface{} { func (e *MockEntry) MetaData() interface{} {
return e.metaData return &e.metaData
} }
func (e *MockEntry) Create(id string, publishedAt *time.Time, metaData model.EntryMetaData) error { func (e *MockEntry) Create(id string, publishedAt *time.Time, metaData model.EntryMetaData) error {
e.id = id e.id = id
e.publishedAt = publishedAt e.publishedAt = publishedAt
e.metaData = metaData.(*MockEntryMetaData) e.metaData = *metaData.(*MockEntryMetaData)
return nil return nil
} }
@ -63,13 +75,27 @@ func TestStructToFields(t *testing.T) {
} }
func TestEditorEntryForm_HtmlForm(t *testing.T) { func TestEditorEntryForm_HtmlForm(t *testing.T) {
formService := editor.NewEditorFormService(&MockEntry{}) form := editor.NewEntryForm(&MockEntry{})
form, err := formService.HtmlForm() html, err := form.HtmlForm()
require.NoError(t, err) require.NoError(t, err)
require.Contains(t, form, "<form") require.Contains(t, html, "<form")
require.Contains(t, form, "method=\"POST\"") require.Contains(t, html, "method=\"POST\"")
require.Contains(t, form, "<input type=\"file\" name=\"Image\" />") require.Contains(t, html, "<input type=\"file\" name=\"Image\"")
require.Contains(t, form, "<input type=\"text\" name=\"Content\" />") require.Contains(t, html, "<input type=\"text\" name=\"Content\"")
require.Contains(t, form, "<input type=\"submit\" value=\"Submit\" />") require.Contains(t, html, "<input type=\"submit\" value=\"Submit\"")
} }
func TestFormParseNil(t *testing.T) {
form := editor.NewEntryForm(&MockEntry{})
_, err := form.Parse(nil)
require.Error(t, err)
}
func TestFormParse(t *testing.T) {
form := editor.NewEntryForm(&MockEntry{})
entry, err := form.Parse(&MockFormData{})
require.NoError(t, err)
require.Equal(t, "Image", entry.MetaData().(*MockEntryMetaData).Image)
require.Equal(t, "Content", entry.MetaData().(*MockEntryMetaData).Content)
}

View File

@ -2,17 +2,31 @@ package editor
import ( import (
"fmt" "fmt"
"mime/multipart"
"owl-blogs/domain/model" "owl-blogs/domain/model"
"reflect" "reflect"
"strings" "strings"
) )
type HttpFormData interface {
// FormFile returns the first file by key from a MultipartForm.
FormFile(key string) (*multipart.FileHeader, error)
// FormValue returns the first value by key from a MultipartForm.
// Search is performed in QueryArgs, PostArgs, MultipartForm and FormFile in this particular order.
// Defaults to the empty string "" if the form value doesn't exist.
// If a default value is given, it will return that value if the form value does not exist.
// Returned value is only valid within the handler. Do not store any references.
// Make copies or use the Immutable setting instead.
FormValue(key string, defaultValue ...string) string
}
type EditorEntryForm struct { type EditorEntryForm struct {
entry model.Entry entry model.Entry
} }
type EntryFormFieldParams struct { type EntryFormFieldParams struct {
InputType string InputType string
Widget string
} }
type EntryFormField struct { type EntryFormField struct {
@ -20,7 +34,7 @@ type EntryFormField struct {
Params EntryFormFieldParams Params EntryFormFieldParams
} }
func NewEditorFormService(entry model.Entry) *EditorEntryForm { func NewEntryForm(entry model.Entry) *EditorEntryForm {
return &EditorEntryForm{ return &EditorEntryForm{
entry: entry, entry: entry,
} }
@ -30,6 +44,8 @@ func (s *EntryFormFieldParams) ApplyTag(tagKey string, tagValue string) error {
switch tagKey { switch tagKey {
case "inputType": case "inputType":
s.InputType = tagValue s.InputType = tagValue
case "widget":
s.Widget = tagValue
default: default:
return fmt.Errorf("unknown tag key: %v", tagKey) return fmt.Errorf("unknown tag key: %v", tagKey)
} }
@ -37,7 +53,14 @@ func (s *EntryFormFieldParams) ApplyTag(tagKey string, tagValue string) error {
} }
func (s *EntryFormField) Html() string { func (s *EntryFormField) Html() string {
return fmt.Sprintf("<input type=\"%v\" name=\"%v\" />\n", s.Params.InputType, s.Name) html := ""
html += fmt.Sprintf("<label for=\"%v\">%v</label>\n", s.Name, s.Name)
if s.Params.InputType == "text" && s.Params.Widget == "textarea" {
html += fmt.Sprintf("<textarea name=\"%v\" id=\"%v\" rows=\"20\"></textarea>\n", s.Name, s.Name)
} else {
html += fmt.Sprintf("<input type=\"%v\" name=\"%v\" id=\"%v\" />\n", s.Params.InputType, s.Name, s.Name)
}
return html
} }
func FieldToFormField(field reflect.StructField) (EntryFormField, error) { func FieldToFormField(field reflect.StructField) (EntryFormField, error) {
@ -89,3 +112,28 @@ func (s *EditorEntryForm) HtmlForm() (string, error) {
return html, nil return html, nil
} }
func (s *EditorEntryForm) Parse(ctx HttpFormData) (model.Entry, error) {
if ctx == nil {
return nil, fmt.Errorf("nil context")
}
meta := s.entry.MetaData()
metaVal := reflect.ValueOf(meta)
if metaVal.Kind() != reflect.Ptr {
return nil, fmt.Errorf("meta data is not a pointer")
}
fields, err := StructToFormFields(meta)
if err != nil {
return nil, err
}
for field := range fields {
fieldName := fields[field].Name
fieldValue := ctx.FormValue(fieldName)
metaField := metaVal.Elem().FieldByName(fieldName)
if metaField.IsValid() {
metaField.SetString(fieldValue)
}
}
return s.entry, nil
}

View File

@ -4,6 +4,7 @@ import (
"owl-blogs/app" "owl-blogs/app"
"owl-blogs/domain/model" "owl-blogs/domain/model"
"owl-blogs/web/editor" "owl-blogs/web/editor"
"time"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
) )
@ -18,15 +19,30 @@ func NewEditorHandler(entryService *app.EntryService) *EditorHandler {
func (h *EditorHandler) HandleGet(c *fiber.Ctx) error { func (h *EditorHandler) HandleGet(c *fiber.Ctx) error {
c.Set(fiber.HeaderContentType, fiber.MIMETextHTML) c.Set(fiber.HeaderContentType, fiber.MIMETextHTML)
formService := editor.NewEditorFormService(&model.ImageEntry{}) form := editor.NewEntryForm(&model.ImageEntry{})
form, err := formService.HtmlForm() htmlForm, err := form.HtmlForm()
if err != nil { if err != nil {
return err return err
} }
return c.SendString(form) return c.SendString(htmlForm)
} }
func (h *EditorHandler) HandlePost(c *fiber.Ctx) error { func (h *EditorHandler) HandlePost(c *fiber.Ctx) error {
c.Set(fiber.HeaderContentType, fiber.MIMETextHTML) c.Set(fiber.HeaderContentType, fiber.MIMETextHTML)
form := editor.NewEntryForm(&model.ImageEntry{})
// get form data
metaData, err := form.Parse(c)
if err != nil {
return err
}
// create entry
now := time.Now()
err = h.entrySvc.Create(&model.ImageEntry{}, &now, metaData)
if err != nil {
return err
}
return c.SendString("Hello, Editor!") return c.SendString("Hello, Editor!")
} }