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/plugings/instagram.go b/plugings/instagram.go index 2e52dba..c95611f 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" ) @@ -20,6 +21,20 @@ type InstagramConfig struct { Password string `owl:"widget=password"` } +// 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( configRepo repository.ConfigRepository, configRegister *app.ConfigRegister, 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/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/web/activity_pub_handler.go b/web/activity_pub_handler.go index d5d5eec..da9ae92 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" @@ -25,6 +26,21 @@ type ActivityPubConfig struct { PrivateKeyPem string `owl:"inputType=text widget=textarea"` } +// 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 { Subject string `json:"subject"` Links []WebfingerLink `json:"links"` 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/forms/form.go b/web/forms/form.go deleted file mode 100644 index 2d2f5f3..0000000 --- a/web/forms/form.go +++ /dev/null @@ -1,185 +0,0 @@ -package forms - -import ( - "fmt" - "owl-blogs/domain/model" - "reflect" - "strings" -) - -type Form[T interface{}] struct { - data T - binSvc model.BinaryStorageInterface -} - -type FormFieldParams struct { - InputType string - Widget string -} - -type FormField struct { - Name string - Value reflect.Value - Params FormFieldParams -} - -func NewForm[T interface{}](data T, binaryService model.BinaryStorageInterface) *Form[T] { - return &Form[T]{ - 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} - case "text": - return &TextWidget{*s} - default: - return &OmitWidget{*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[T]) 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[T]) Parse(ctx model.HttpFormData) (T, error) { - var empty T - - if ctx == nil { - return empty, fmt.Errorf("nil context") - } - dataVal := reflect.ValueOf(s.data) - if dataVal.Kind() != reflect.Ptr { - return empty, fmt.Errorf("meta data is not a pointer") - } - fields, err := StructToFormFields(s.data) - if err != nil { - return empty, 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 empty, err - } - fileData, err := file.Open() - if err != nil { - return empty, err - } - defer fileData.Close() - fileBytes := make([]byte, file.Size) - _, err = fileData.Read(fileBytes) - if err != nil { - return empty, err - } - - binaryFile, err := s.binSvc.Create(file.Filename, fileBytes) - if err != nil { - return empty, 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 eb011ce..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 OmitWidget struct { - FormField -} - -func (s *OmitWidget) Html() string { - html := "" - return html -} - -func (s *OmitWidget) ParseValue(value string, output reflect.Value) error { - 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 -}