diff --git a/app/config_register.go b/app/config_register.go index 22d5f8c..01765a3 100644 --- a/app/config_register.go +++ b/app/config_register.go @@ -1,19 +1,26 @@ package app +import "owl-blogs/domain/model" + +type AppConfig interface { + Form(binSvc model.BinaryStorageInterface) string + ParseFormData(data model.HttpFormData, binSvc model.BinaryStorageInterface) (AppConfig, error) +} + type ConfigRegister struct { - configs map[string]interface{} + configs map[string]AppConfig } type RegisteredConfig struct { Name string - Config interface{} + Config AppConfig } func NewConfigRegister() *ConfigRegister { - return &ConfigRegister{configs: map[string]interface{}{}} + return &ConfigRegister{configs: map[string]AppConfig{}} } -func (r *ConfigRegister) Register(name string, config interface{}) { +func (r *ConfigRegister) Register(name string, config AppConfig) { r.configs[name] = config } @@ -28,6 +35,6 @@ func (r *ConfigRegister) Configs() []RegisteredConfig { return configs } -func (r *ConfigRegister) GetConfig(name string) interface{} { +func (r *ConfigRegister) GetConfig(name string) AppConfig { return r.configs[name] } diff --git a/cmd/owl/editor_test.go b/cmd/owl/editor_test.go index d7e4a56..7d570b6 100644 --- a/cmd/owl/editor_test.go +++ b/cmd/owl/editor_test.go @@ -77,9 +77,9 @@ func TestEditorFormPost(t *testing.T) { body := &bytes.Buffer{} writer := multipart.NewWriter(body) - part, _ := writer.CreateFormFile("ImageId", filepath.Base(file.Name())) + part, _ := writer.CreateFormFile("image", filepath.Base(file.Name())) io.Copy(part, file) - part, _ = writer.CreateFormField("Content") + part, _ = writer.CreateFormField("content") io.WriteString(part, "test content") writer.Close() diff --git a/domain/model/binary_service.go b/domain/model/binary_service.go new file mode 100644 index 0000000..5d33e34 --- /dev/null +++ b/domain/model/binary_service.go @@ -0,0 +1,5 @@ +package model + +type BinaryStorageInterface interface { + Create(name string, file []byte) (*BinaryFile, error) +} diff --git a/domain/model/entry.go b/domain/model/entry.go index b9372c2..84a4a57 100644 --- a/domain/model/entry.go +++ b/domain/model/entry.go @@ -1,6 +1,8 @@ package model -import "time" +import ( + "time" +) type EntryContent string @@ -9,18 +11,20 @@ type Entry interface { Content() EntryContent PublishedAt() *time.Time AuthorId() string - MetaData() interface{} + MetaData() EntryMetaData // Optional: can return empty string Title() string SetID(id string) SetPublishedAt(publishedAt *time.Time) - SetMetaData(metaData interface{}) + SetMetaData(metaData EntryMetaData) SetAuthorId(authorId string) } type EntryMetaData interface { + Form(binSvc BinaryStorageInterface) string + ParseFormData(data HttpFormData, binSvc BinaryStorageInterface) (EntryMetaData, error) } type EntryBase struct { diff --git a/domain/model/form_data.go b/domain/model/form_data.go new file mode 100644 index 0000000..396120b --- /dev/null +++ b/domain/model/form_data.go @@ -0,0 +1,15 @@ +package model + +import "mime/multipart" + +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 +} diff --git a/entry_types/article.go b/entry_types/article.go index 9849a24..8cd10ad 100644 --- a/entry_types/article.go +++ b/entry_types/article.go @@ -12,8 +12,22 @@ type Article struct { } type ArticleMetaData struct { - Title string `owl:"inputType=text"` - Content string `owl:"inputType=text widget=textarea"` + Title string + Content string +} + +// Form implements model.EntryMetaData. +func (meta *ArticleMetaData) Form(binSvc model.BinaryStorageInterface) string { + f, _ := render.RenderTemplateToString("forms/Article", meta) + return f +} + +// ParseFormData implements model.EntryMetaData. +func (*ArticleMetaData) ParseFormData(data model.HttpFormData, binSvc model.BinaryStorageInterface) (model.EntryMetaData, error) { + return &ArticleMetaData{ + Title: data.FormValue("title"), + Content: data.FormValue("content"), + }, nil } func (e *Article) Title() string { @@ -28,10 +42,10 @@ func (e *Article) Content() model.EntryContent { return model.EntryContent(str) } -func (e *Article) MetaData() interface{} { +func (e *Article) MetaData() model.EntryMetaData { return &e.meta } -func (e *Article) SetMetaData(metaData interface{}) { +func (e *Article) SetMetaData(metaData model.EntryMetaData) { e.meta = *metaData.(*ArticleMetaData) } diff --git a/entry_types/bookmark.go b/entry_types/bookmark.go index 2002bd0..19b5d3e 100644 --- a/entry_types/bookmark.go +++ b/entry_types/bookmark.go @@ -12,9 +12,24 @@ type Bookmark struct { } type BookmarkMetaData struct { - Title string `owl:"inputType=text"` - Url string `owl:"inputType=text"` - Content string `owl:"inputType=text widget=textarea"` + Title string + Url string + Content string +} + +// Form implements model.EntryMetaData. +func (meta *BookmarkMetaData) Form(binSvc model.BinaryStorageInterface) string { + f, _ := render.RenderTemplateToString("forms/Bookmark", meta) + return f +} + +// ParseFormData implements model.EntryMetaData. +func (*BookmarkMetaData) ParseFormData(data model.HttpFormData, binSvc model.BinaryStorageInterface) (model.EntryMetaData, error) { + return &BookmarkMetaData{ + Title: data.FormValue("title"), + Url: data.FormValue("url"), + Content: data.FormValue("content"), + }, nil } func (e *Bookmark) Title() string { @@ -29,10 +44,10 @@ func (e *Bookmark) Content() model.EntryContent { return model.EntryContent(str) } -func (e *Bookmark) MetaData() interface{} { +func (e *Bookmark) MetaData() model.EntryMetaData { return &e.meta } -func (e *Bookmark) SetMetaData(metaData interface{}) { +func (e *Bookmark) SetMetaData(metaData model.EntryMetaData) { e.meta = *metaData.(*BookmarkMetaData) } diff --git a/entry_types/image.go b/entry_types/image.go index bd23e37..acb9cdc 100644 --- a/entry_types/image.go +++ b/entry_types/image.go @@ -12,9 +12,47 @@ type Image struct { } type ImageMetaData struct { - ImageId string `owl:"inputType=file"` - Title string `owl:"inputType=text"` - Content string `owl:"inputType=text widget=textarea"` + ImageId string + Title string + Content string +} + +// Form implements model.EntryMetaData. +func (meta *ImageMetaData) Form(binSvc model.BinaryStorageInterface) string { + f, _ := render.RenderTemplateToString("forms/Image", meta) + return f +} + +// ParseFormData implements model.EntryMetaData. +func (meta *ImageMetaData) ParseFormData(data model.HttpFormData, binSvc model.BinaryStorageInterface) (model.EntryMetaData, error) { + file, err := data.FormFile("image") + var imgId = meta.ImageId + if err != nil && imgId == "" { + return nil, err + } else if err == nil { + fileData, err := file.Open() + if err != nil { + return nil, err + } + defer fileData.Close() + + fileBytes := make([]byte, file.Size) + _, err = fileData.Read(fileBytes) + if err != nil { + return nil, err + } + bin, err := binSvc.Create(file.Filename, fileBytes) + if err != nil { + return nil, err + } + imgId = bin.Id + } + + return &ImageMetaData{ + ImageId: imgId, + Title: data.FormValue("title"), + Content: data.FormValue("content"), + }, nil } func (e *Image) Title() string { @@ -29,10 +67,10 @@ func (e *Image) Content() model.EntryContent { return model.EntryContent(str) } -func (e *Image) MetaData() interface{} { +func (e *Image) MetaData() model.EntryMetaData { return &e.meta } -func (e *Image) SetMetaData(metaData interface{}) { +func (e *Image) SetMetaData(metaData model.EntryMetaData) { e.meta = *metaData.(*ImageMetaData) } diff --git a/entry_types/note.go b/entry_types/note.go index 590e9a5..f828465 100644 --- a/entry_types/note.go +++ b/entry_types/note.go @@ -12,7 +12,20 @@ type Note struct { } type NoteMetaData struct { - Content string `owl:"inputType=text widget=textarea"` + Content string +} + +// Form implements model.EntryMetaData. +func (meta *NoteMetaData) Form(binSvc model.BinaryStorageInterface) string { + f, _ := render.RenderTemplateToString("forms/Note", meta) + return f +} + +// ParseFormData implements model.EntryMetaData. +func (*NoteMetaData) ParseFormData(data model.HttpFormData, binSvc model.BinaryStorageInterface) (model.EntryMetaData, error) { + return &NoteMetaData{ + Content: data.FormValue("content"), + }, nil } func (e *Note) Title() string { @@ -27,10 +40,10 @@ func (e *Note) Content() model.EntryContent { return model.EntryContent(str) } -func (e *Note) MetaData() interface{} { +func (e *Note) MetaData() model.EntryMetaData { return &e.meta } -func (e *Note) SetMetaData(metaData interface{}) { +func (e *Note) SetMetaData(metaData model.EntryMetaData) { e.meta = *metaData.(*NoteMetaData) } diff --git a/entry_types/page.go b/entry_types/page.go index 78faece..37a7c97 100644 --- a/entry_types/page.go +++ b/entry_types/page.go @@ -12,8 +12,22 @@ type Page struct { } type PageMetaData struct { - Title string `owl:"inputType=text"` - Content string `owl:"inputType=text widget=textarea"` + Title string + Content string +} + +// Form implements model.EntryMetaData. +func (meta *PageMetaData) Form(binSvc model.BinaryStorageInterface) string { + f, _ := render.RenderTemplateToString("forms/Page", meta) + return f +} + +// ParseFormData implements model.EntryMetaData. +func (*PageMetaData) ParseFormData(data model.HttpFormData, binSvc model.BinaryStorageInterface) (model.EntryMetaData, error) { + return &PageMetaData{ + Title: data.FormValue("title"), + Content: data.FormValue("content"), + }, nil } func (e *Page) Title() string { @@ -28,10 +42,10 @@ func (e *Page) Content() model.EntryContent { return model.EntryContent(str) } -func (e *Page) MetaData() interface{} { +func (e *Page) MetaData() model.EntryMetaData { return &e.meta } -func (e *Page) SetMetaData(metaData interface{}) { +func (e *Page) SetMetaData(metaData model.EntryMetaData) { e.meta = *metaData.(*PageMetaData) } diff --git a/entry_types/recipe.go b/entry_types/recipe.go index d6c8a51..f0eb117 100644 --- a/entry_types/recipe.go +++ b/entry_types/recipe.go @@ -4,6 +4,7 @@ import ( "fmt" "owl-blogs/domain/model" "owl-blogs/render" + "strings" ) type Recipe struct { @@ -12,11 +13,35 @@ type Recipe struct { } type RecipeMetaData struct { - Title string `owl:"inputType=text"` - Yield string `owl:"inputType=text"` - Duration string `owl:"inputType=text"` - Ingredients []string `owl:"inputType=text widget=textlist"` - Content string `owl:"inputType=text widget=textarea"` + Title string + Yield string + Duration string + Ingredients []string + Content string +} + +// Form implements model.EntryMetaData. +func (meta *RecipeMetaData) Form(binSvc model.BinaryStorageInterface) string { + f, _ := render.RenderTemplateToString("forms/Recipe", meta) + return f +} + +// ParseFormData implements model.EntryMetaData. +func (*RecipeMetaData) ParseFormData(data model.HttpFormData, binSvc model.BinaryStorageInterface) (model.EntryMetaData, error) { + ings := strings.Split(data.FormValue("ingredients"), "\n") + clean := make([]string, 0) + for _, ing := range ings { + if strings.TrimSpace(ing) != "" { + clean = append(clean, strings.TrimSpace(ing)) + } + } + return &RecipeMetaData{ + Title: data.FormValue("title"), + Yield: data.FormValue("yield"), + Duration: data.FormValue("duration"), + Ingredients: clean, + Content: data.FormValue("content"), + }, nil } func (e *Recipe) Title() string { @@ -31,10 +56,10 @@ func (e *Recipe) Content() model.EntryContent { return model.EntryContent(str) } -func (e *Recipe) MetaData() interface{} { +func (e *Recipe) MetaData() model.EntryMetaData { return &e.meta } -func (e *Recipe) SetMetaData(metaData interface{}) { +func (e *Recipe) SetMetaData(metaData model.EntryMetaData) { e.meta = *metaData.(*RecipeMetaData) } diff --git a/entry_types/reply.go b/entry_types/reply.go index de2f751..1e088f6 100644 --- a/entry_types/reply.go +++ b/entry_types/reply.go @@ -12,9 +12,24 @@ type Reply struct { } type ReplyMetaData struct { - Title string `owl:"inputType=text"` - Url string `owl:"inputType=text"` - Content string `owl:"inputType=text widget=textarea"` + Title string + Url string + Content string +} + +// Form implements model.EntryMetaData. +func (meta *ReplyMetaData) Form(binSvc model.BinaryStorageInterface) string { + f, _ := render.RenderTemplateToString("forms/Reply", meta) + return f +} + +// ParseFormData implements model.EntryMetaData. +func (*ReplyMetaData) ParseFormData(data model.HttpFormData, binSvc model.BinaryStorageInterface) (model.EntryMetaData, error) { + return &ReplyMetaData{ + Title: data.FormValue("title"), + Url: data.FormValue("url"), + Content: data.FormValue("content"), + }, nil } func (e *Reply) Title() string { @@ -29,10 +44,10 @@ func (e *Reply) Content() model.EntryContent { return model.EntryContent(str) } -func (e *Reply) MetaData() interface{} { +func (e *Reply) MetaData() model.EntryMetaData { return &e.meta } -func (e *Reply) SetMetaData(metaData interface{}) { +func (e *Reply) SetMetaData(metaData model.EntryMetaData) { e.meta = *metaData.(*ReplyMetaData) } diff --git a/infra/entry_repository.go b/infra/entry_repository.go index e4fc563..bde324a 100644 --- a/infra/entry_repository.go +++ b/infra/entry_repository.go @@ -151,7 +151,7 @@ func (r *DefaultEntryRepo) sqlEntryToEntry(entry sqlEntry) (model.Entry, error) if err != nil { return nil, errors.New("entry type not registered") } - metaData := reflect.New(reflect.TypeOf(e.MetaData()).Elem()).Interface() + metaData := reflect.New(reflect.TypeOf(e.MetaData()).Elem()).Interface().(model.EntryMetaData) json.Unmarshal([]byte(*entry.MetaData), metaData) e.SetID(entry.Id) e.SetPublishedAt(entry.PublishedAt) diff --git a/plugings/instagram.go b/plugings/instagram.go index 2e52dba..5e045c4 100644 --- a/plugings/instagram.go +++ b/plugings/instagram.go @@ -6,6 +6,7 @@ import ( "owl-blogs/app/repository" "owl-blogs/domain/model" entrytypes "owl-blogs/entry_types" + "owl-blogs/render" "github.com/Davincible/goinsta/v3" ) @@ -16,8 +17,22 @@ type Instagram struct { } type InstagramConfig struct { - User string `owl:"widget=text"` - Password string `owl:"widget=password"` + User string + Password string +} + +// Form implements app.AppConfig. +func (cfg *InstagramConfig) Form(binSvc model.BinaryStorageInterface) string { + f, _ := render.RenderTemplateToString("forms/InstagramConfig", cfg) + return f +} + +// ParseFormData implements app.AppConfig. +func (*InstagramConfig) ParseFormData(data model.HttpFormData, binSvc model.BinaryStorageInterface) (app.AppConfig, error) { + return &InstagramConfig{ + User: data.FormValue("User"), + Password: data.FormValue("Password"), + }, nil } func RegisterInstagram( diff --git a/render/templates/forms/ActivityPubConfig.tmpl b/render/templates/forms/ActivityPubConfig.tmpl new file mode 100644 index 0000000..596bebc --- /dev/null +++ b/render/templates/forms/ActivityPubConfig.tmpl @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/render/templates/forms/Article.tmpl b/render/templates/forms/Article.tmpl new file mode 100644 index 0000000..3b5e9ce --- /dev/null +++ b/render/templates/forms/Article.tmpl @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/render/templates/forms/Bookmark.tmpl b/render/templates/forms/Bookmark.tmpl new file mode 100644 index 0000000..6e69609 --- /dev/null +++ b/render/templates/forms/Bookmark.tmpl @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/render/templates/forms/Image.tmpl b/render/templates/forms/Image.tmpl new file mode 100644 index 0000000..9286c42 --- /dev/null +++ b/render/templates/forms/Image.tmpl @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/render/templates/forms/InstagramConfig.tmpl b/render/templates/forms/InstagramConfig.tmpl new file mode 100644 index 0000000..c131350 --- /dev/null +++ b/render/templates/forms/InstagramConfig.tmpl @@ -0,0 +1,5 @@ + + + + + diff --git a/render/templates/forms/Note.tmpl b/render/templates/forms/Note.tmpl new file mode 100644 index 0000000..63cb09d --- /dev/null +++ b/render/templates/forms/Note.tmpl @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/render/templates/forms/Page.tmpl b/render/templates/forms/Page.tmpl new file mode 100644 index 0000000..3b5e9ce --- /dev/null +++ b/render/templates/forms/Page.tmpl @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/render/templates/forms/Recipe.tmpl b/render/templates/forms/Recipe.tmpl new file mode 100644 index 0000000..bcf27bc --- /dev/null +++ b/render/templates/forms/Recipe.tmpl @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/render/templates/forms/Reply.tmpl b/render/templates/forms/Reply.tmpl new file mode 100644 index 0000000..aaca489 --- /dev/null +++ b/render/templates/forms/Reply.tmpl @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/test/mock_entry.go b/test/mock_entry.go index 1b0373e..a21754f 100644 --- a/test/mock_entry.go +++ b/test/mock_entry.go @@ -12,6 +12,16 @@ type MockEntryMetaData struct { Title string } +// Form implements model.EntryMetaData. +func (*MockEntryMetaData) Form(binSvc model.BinaryStorageInterface) string { + panic("unimplemented") +} + +// ParseFormData implements model.EntryMetaData. +func (*MockEntryMetaData) ParseFormData(data model.HttpFormData, binSvc model.BinaryStorageInterface) (model.EntryMetaData, error) { + panic("unimplemented") +} + type MockEntry struct { model.EntryBase metaData *MockEntryMetaData @@ -21,11 +31,11 @@ func (e *MockEntry) Content() model.EntryContent { return model.EntryContent(e.metaData.Str) } -func (e *MockEntry) MetaData() interface{} { +func (e *MockEntry) MetaData() model.EntryMetaData { return e.metaData } -func (e *MockEntry) SetMetaData(metaData interface{}) { +func (e *MockEntry) SetMetaData(metaData model.EntryMetaData) { e.metaData = metaData.(*MockEntryMetaData) } diff --git a/tmp/build-errors.log b/tmp/build-errors.log deleted file mode 100644 index 05e5985..0000000 --- a/tmp/build-errors.log +++ /dev/null @@ -1 +0,0 @@ -exit status 1 \ No newline at end of file diff --git a/web/activity_pub_handler.go b/web/activity_pub_handler.go index d5d5eec..6783184 100644 --- a/web/activity_pub_handler.go +++ b/web/activity_pub_handler.go @@ -6,6 +6,7 @@ import ( "owl-blogs/app/repository" "owl-blogs/config" "owl-blogs/domain/model" + "owl-blogs/render" vocab "github.com/go-ap/activitypub" @@ -20,9 +21,24 @@ type ActivityPubServer struct { } type ActivityPubConfig struct { - PreferredUsername string `owl:"inputType=text"` - PublicKeyPem string `owl:"inputType=text widget=textarea"` - PrivateKeyPem string `owl:"inputType=text widget=textarea"` + PreferredUsername string + PublicKeyPem string + PrivateKeyPem string +} + +// Form implements app.AppConfig. +func (cfg *ActivityPubConfig) Form(binSvc model.BinaryStorageInterface) string { + f, _ := render.RenderTemplateToString("forms/ActivityPubConfig", cfg) + return f +} + +// ParseFormData implements app.AppConfig. +func (*ActivityPubConfig) ParseFormData(data model.HttpFormData, binSvc model.BinaryStorageInterface) (app.AppConfig, error) { + return &ActivityPubConfig{ + PreferredUsername: data.FormValue("PreferredUsername"), + PublicKeyPem: data.FormValue("PublicKeyPem"), + PrivateKeyPem: data.FormValue("PrivateKeyPem"), + }, nil } type WebfingerResponse struct { diff --git a/web/admin_handler.go b/web/admin_handler.go index 126a750..eb249f5 100644 --- a/web/admin_handler.go +++ b/web/admin_handler.go @@ -4,7 +4,6 @@ import ( "owl-blogs/app" "owl-blogs/app/repository" "owl-blogs/render" - "owl-blogs/web/forms" "sort" "github.com/gofiber/fiber/v2" @@ -14,6 +13,7 @@ type adminHandler struct { configRepo repository.ConfigRepository configRegister *app.ConfigRegister typeRegistry *app.EntryTypeRegistry + binSvc *app.BinaryService } type adminContet struct { @@ -75,8 +75,7 @@ func (h *adminHandler) HandleConfigGet(c *fiber.Ctx) error { } siteConfig := getSiteConfig(h.configRepo) - form := forms.NewForm(config, nil) - htmlForm, err := form.HtmlForm() + htmlForm := config.Form(h.binSvc) if err != nil { return err } @@ -93,9 +92,7 @@ func (h *adminHandler) HandleConfigPost(c *fiber.Ctx) error { return c.SendStatus(404) } - form := forms.NewForm(config, nil) - - newConfig, err := form.Parse(c) + newConfig, err := config.ParseFormData(c, h.binSvc) if err != nil { return err } diff --git a/web/editor_handler.go b/web/editor_handler.go index 86e1be8..710ae85 100644 --- a/web/editor_handler.go +++ b/web/editor_handler.go @@ -5,7 +5,6 @@ import ( "owl-blogs/app/repository" "owl-blogs/domain/model" "owl-blogs/render" - "owl-blogs/web/forms" "time" "github.com/gofiber/fiber/v2" @@ -48,12 +47,7 @@ func (h *EditorHandler) HandleGetNew(c *fiber.Ctx) error { if err != nil { return err } - - form := forms.NewForm(entryType.MetaData(), h.binSvc) - htmlForm, err := form.HtmlForm() - if err != nil { - return err - } + htmlForm := entryType.MetaData().Form(h.binSvc) return render.RenderTemplateWithBase(c, getSiteConfig(h.configRepo), "views/editor", htmlForm) } @@ -65,9 +59,7 @@ func (h *EditorHandler) HandlePostNew(c *fiber.Ctx) error { return err } - form := forms.NewForm(entry.MetaData(), h.binSvc) - // get form data - entryMeta, err := form.Parse(c) + entryMeta, err := entry.MetaData().ParseFormData(c, h.binSvc) if err != nil { return err } @@ -100,11 +92,7 @@ func (h *EditorHandler) HandleGetEdit(c *fiber.Ctx) error { return err } - form := forms.NewForm(entry.MetaData(), h.binSvc) - htmlForm, err := form.HtmlForm() - if err != nil { - return err - } + htmlForm := entry.MetaData().Form(h.binSvc) return render.RenderTemplateWithBase(c, getSiteConfig(h.configRepo), "views/editor", htmlForm) } @@ -117,9 +105,8 @@ func (h *EditorHandler) HandlePostEdit(c *fiber.Ctx) error { return err } - form := forms.NewForm(entry.MetaData(), h.binSvc) // get form data - meta, err := form.Parse(c) + meta, err := entry.MetaData().ParseFormData(c, h.binSvc) if err != nil { return err } diff --git a/web/forms/form.go b/web/forms/form.go deleted file mode 100644 index 1deab80..0000000 --- a/web/forms/form.go +++ /dev/null @@ -1,194 +0,0 @@ -package forms - -import ( - "fmt" - "mime/multipart" - "owl-blogs/app" - "reflect" - "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 Form struct { - data interface{} - binSvc *app.BinaryService -} - -type FormFieldParams struct { - InputType string - Widget string -} - -type FormField struct { - Name string - Value reflect.Value - Params FormFieldParams -} - -func NewForm(data interface{}, binaryService *app.BinaryService) *Form { - return &Form{ - data: data, - binSvc: binaryService, - } -} - -func (s *FormFieldParams) ApplyTag(tagKey string, tagValue string) error { - switch tagKey { - case "inputType": - s.InputType = tagValue - case "widget": - s.Widget = tagValue - default: - return fmt.Errorf("unknown tag key: %v", tagKey) - } - return nil -} - -func (s *FormField) ToWidget() Widget { - switch s.Params.Widget { - case "textarea": - return &TextareaWidget{*s} - case "textlist": - return &TextListWidget{*s} - case "password": - return &PasswordWidget{*s} - default: - return &TextWidget{*s} - } -} - -func (s *FormField) Html() string { - html := "" - html += fmt.Sprintf("\n", s.Name, s.Name) - if s.Params.InputType == "file" { - html += fmt.Sprintf("\n", s.Params.InputType, s.Name, s.Name, s.Value) - } else { - html += s.ToWidget().Html() - html += "\n" - } - return html -} - -func FieldToFormField(field reflect.StructField, value reflect.Value) (FormField, error) { - formField := FormField{ - Name: field.Name, - Value: value, - Params: FormFieldParams{}, - } - tag := field.Tag.Get("owl") - for _, param := range strings.Split(tag, " ") { - parts := strings.Split(param, "=") - if len(parts) != 2 { - continue - } - err := formField.Params.ApplyTag(parts[0], parts[1]) - if err != nil { - return FormField{}, err - } - } - return formField, nil -} - -func StructToFormFields(data interface{}) ([]FormField, error) { - dataValue := reflect.Indirect(reflect.ValueOf(data)) - dataType := reflect.TypeOf(data).Elem() - numFields := dataType.NumField() - fields := []FormField{} - for i := 0; i < numFields; i++ { - field, err := FieldToFormField( - dataType.Field(i), - dataValue.FieldByIndex([]int{i}), - ) - if err != nil { - return nil, err - } - fields = append(fields, field) - } - return fields, nil -} - -func (s *Form) HtmlForm() (string, error) { - fields, err := StructToFormFields(s.data) - if err != nil { - return "", err - } - - html := "" - for _, field := range fields { - html += field.Html() - } - - return html, nil -} - -func (s *Form) Parse(ctx HttpFormData) (interface{}, error) { - if ctx == nil { - return nil, fmt.Errorf("nil context") - } - dataVal := reflect.ValueOf(s.data) - if dataVal.Kind() != reflect.Ptr { - return nil, fmt.Errorf("meta data is not a pointer") - } - fields, err := StructToFormFields(s.data) - if err != nil { - return nil, err - } - for _, field := range fields { - fieldName := field.Name - - if field.Params.InputType == "file" { - file, err := ctx.FormFile(fieldName) - if err != nil { - // If field already has a value, we can ignore the error - if field.Value != reflect.Zero(field.Value.Type()) { - metaField := dataVal.Elem().FieldByName(fieldName) - if metaField.IsValid() { - metaField.SetString(field.Value.String()) - } - continue - } - return nil, err - } - fileData, err := file.Open() - if err != nil { - return nil, err - } - defer fileData.Close() - fileBytes := make([]byte, file.Size) - _, err = fileData.Read(fileBytes) - if err != nil { - return nil, err - } - - binaryFile, err := s.binSvc.Create(file.Filename, fileBytes) - if err != nil { - return nil, err - } - - metaField := dataVal.Elem().FieldByName(fieldName) - if metaField.IsValid() { - metaField.SetString(binaryFile.Id) - } - } else { - formValue := ctx.FormValue(fieldName) - metaField := dataVal.Elem().FieldByName(fieldName) - if metaField.IsValid() { - field.ToWidget().ParseValue(formValue, metaField) - } - } - - } - - return s.data, nil -} diff --git a/web/forms/form_test.go b/web/forms/form_test.go deleted file mode 100644 index bd1bb56..0000000 --- a/web/forms/form_test.go +++ /dev/null @@ -1,108 +0,0 @@ -package forms_test - -import ( - "bytes" - "io" - "mime/multipart" - "os" - "owl-blogs/app" - "owl-blogs/infra" - "owl-blogs/test" - "owl-blogs/web/forms" - "path" - "path/filepath" - "reflect" - "testing" - - "github.com/stretchr/testify/require" -) - -type MockData struct { - Image string `owl:"inputType=file"` - Content string `owl:"inputType=text"` -} - -type MockFormData struct { - fileHeader *multipart.FileHeader -} - -func NewMockFormData() *MockFormData { - fileDir, _ := os.Getwd() - fileName := "../../test/fixtures/test.png" - filePath := path.Join(fileDir, fileName) - - file, err := os.Open(filePath) - if err != nil { - panic(err) - } - defer file.Close() - - body := &bytes.Buffer{} - writer := multipart.NewWriter(body) - part, err := writer.CreateFormFile("ImagePath", filepath.Base(file.Name())) - if err != nil { - panic(err) - } - io.Copy(part, file) - writer.Close() - - multipartForm := multipart.NewReader(body, writer.Boundary()) - formData, err := multipartForm.ReadForm(0) - if err != nil { - panic(err) - } - fileHeader := formData.File["ImagePath"][0] - - return &MockFormData{fileHeader: fileHeader} -} - -func (f *MockFormData) FormFile(key string) (*multipart.FileHeader, error) { - return f.fileHeader, nil -} - -func (f *MockFormData) FormValue(key string, defaultValue ...string) string { - return key -} - -func TestFieldToFormField(t *testing.T) { - field := reflect.TypeOf(&MockData{}).Elem().Field(0) - formField, err := forms.FieldToFormField(field, reflect.Value{}) - require.NoError(t, err) - require.Equal(t, "Image", formField.Name) - require.Equal(t, "file", formField.Params.InputType) -} - -func TestStructToFields(t *testing.T) { - fields, err := forms.StructToFormFields(&MockData{}) - require.NoError(t, err) - require.Len(t, fields, 2) - require.Equal(t, "Image", fields[0].Name) - require.Equal(t, "file", fields[0].Params.InputType) - require.Equal(t, "Content", fields[1].Name) - require.Equal(t, "text", fields[1].Params.InputType) -} - -func TestForm_HtmlForm(t *testing.T) { - form := forms.NewForm(&MockData{}, nil) - html, err := form.HtmlForm() - require.NoError(t, err) - require.Contains(t, html, "\n", s.Name, s.Value.String()) - return html -} - -func (s *TextWidget) ParseValue(value string, output reflect.Value) error { - output.SetString(value) - return nil -} - -type PasswordWidget struct { - FormField -} - -func (s *PasswordWidget) Html() string { - html := "" - html += fmt.Sprintf("\n", s.Name, s.Value.String()) - return html -} - -func (s *PasswordWidget) ParseValue(value string, output reflect.Value) error { - output.SetString(value) - return nil -} - -type TextareaWidget struct { - FormField -} - -func (s *TextareaWidget) Html() string { - html := "" - html += fmt.Sprintf("\n", s.Name, s.Value.String()) - return html -} - -func (s *TextareaWidget) ParseValue(value string, output reflect.Value) error { - output.SetString(value) - return nil -} - -type TextListWidget struct { - FormField -} - -func (s *TextListWidget) Html() string { - valueList := s.Value.Interface().([]string) - value := strings.Join(valueList, "\n") - - html := "" - html += fmt.Sprintf("\n", s.Name, value) - return html -} - -func (s *TextListWidget) ParseValue(value string, output reflect.Value) error { - list := strings.Split(value, "\n") - // trim entries - for i, item := range list { - list[i] = strings.TrimSpace(item) - } - // remove empty entries - for i := len(list) - 1; i >= 0; i-- { - if list[i] == "" { - list = append(list[:i], list[i+1:]...) - } - } - - output.Set(reflect.ValueOf(list)) - return nil -}