From 31dd3b2e82387b9c8350513dd05d2f521aedd6b7 Mon Sep 17 00:00:00 2001 From: Thomas Rabaix Date: Thu, 12 Oct 2023 00:40:29 +0100 Subject: [PATCH] feat(form): rework code to work a more use cases --- core/form/form_conversion.go | 12 +- core/form/form_marshaller.go | 223 ++++++++++++++++++++++----------- core/form/form_structs.go | 33 +++-- core/form/form_structs_test.go | 48 +++---- 4 files changed, 199 insertions(+), 117 deletions(-) diff --git a/core/form/form_conversion.go b/core/form/form_conversion.go index 494825e..febc44a 100644 --- a/core/form/form_conversion.go +++ b/core/form/form_conversion.go @@ -15,12 +15,16 @@ func yes(value string) bool { return value == "checked" || value == "true" || value == "1" || value == "on" || value == "yes" } +func no(value string) bool { + return value == "false" || value == "0" || value == "off" || value == "no" +} + func BoolToStr(v interface{}) (string, bool) { if v, ok := v.(bool); ok { if v { - return "yes", true + return "true", true } else { - return "no", true + return "false", true } } @@ -28,9 +32,9 @@ func BoolToStr(v interface{}) (string, bool) { } func StrToBool(value interface{}) (bool, bool) { - if value == "yes" { + if yes(value.(string)) { return true, true - } else if value == "no" { + } else if no(value.(string)) { return false, true } diff --git a/core/form/form_marshaller.go b/core/form/form_marshaller.go index 9dda3d4..2ffcdbe 100644 --- a/core/form/form_marshaller.go +++ b/core/form/form_marshaller.go @@ -26,32 +26,38 @@ func iterateFields(form *Form, fields []*FormField) { } } -func configure(field *FormField, form *Form) { - var rv reflect.Value +func marshal(field *FormField, form *Form) { + field.Marshaller(field, form) +} - if form.Data != nil && field.InitialValue == nil { - field.reflectField = form.reflect.FieldByName(field.Name) - rv = field.reflectField - } else if field.InitialValue != nil { - field.reflectValue = reflect.ValueOf(field.InitialValue) - rv = field.reflectValue - } else { - panic(fmt.Sprintf("Unable to find the field type: %s, %v", field.Name, field.InitialValue)) - } +func unmarshal(field *FormField, form *Form, values url.Values) { + field.Errors = []string{} - if rv.Kind() != reflect.Invalid { - field.InitialValue = rv.Interface() + if values.Has(field.Input.Name) { + field.Input.Value = values.Get(field.Input.Name) } - var marshaller Marshaller = defaultMarshal - var unmarshaller Unmarshaller = defaultUnmarshal - kind := "text" + field.Unmarshaller(field, form, values) + field.HasErrors = len(field.Errors) > 0 +} + +type MarshallerResult struct { + Marshaller Marshaller + Unmarshaller Unmarshaller + Type string +} + +func findMarshaller(rv reflect.Value) *MarshallerResult { if rv.Kind() == reflect.String { - marshaller = defaultMarshal - unmarshaller = defaultUnmarshal - kind = "text" - } else if rv.Kind() == reflect.Int || + return &MarshallerResult{ + Marshaller: defaultMarshal, + Unmarshaller: defaultUnmarshal, + Type: "text", + } + } + + if rv.Kind() == reflect.Int || rv.Kind() == reflect.Int8 || rv.Kind() == reflect.Int16 || rv.Kind() == reflect.Int32 || @@ -63,59 +69,85 @@ func configure(field *FormField, form *Form) { rv.Kind() == reflect.Uint64 || rv.Kind() == reflect.Float32 || rv.Kind() == reflect.Float64 { - marshaller = numberMarshal - unmarshaller = numberUnmarshal - kind = "number" - } else if rv.Kind() == reflect.Bool { - marshaller = booleanMarshal - unmarshaller = booleanUnmarshal - kind = "boolean" - - } else if rv.Kind() == reflect.Struct { + + return &MarshallerResult{ + Marshaller: numberMarshal, + Unmarshaller: numberUnmarshal, + Type: "number", + } + } + + if rv.Kind() == reflect.Bool { + return &MarshallerResult{ + Marshaller: booleanMarshal, + Unmarshaller: booleanUnmarshal, + Type: "boolean", + } + } + + if rv.Kind() == reflect.Struct { if rv.Type() == reflect.TypeOf(time.Time{}) { - marshaller = dateMarshal - unmarshaller = dateUnmarshal - kind = "date" + return &MarshallerResult{ + Marshaller: dateMarshal, + Unmarshaller: dateUnmarshal, + Type: "date", + } } - } else if rv.Kind() == reflect.Ptr { + } + + if rv.Kind() == reflect.Ptr { if rv.Type() == reflect.TypeOf(&Form{}) { - marshaller = formMarshal - unmarshaller = formUnmarshal - kind = "form" - } else if rv.Type() == reflect.TypeOf(&FieldCollectionOptions{}) { - marshaller = collectionMarshal - unmarshaller = collectionUnmarshal - kind = "collection" + return &MarshallerResult{ + Marshaller: formMarshal, + Unmarshaller: formUnmarshal, + Type: "form", + } + } + + if rv.Type() == reflect.TypeOf(&FieldCollectionOptions{}) { + return &MarshallerResult{ + Marshaller: collectionMarshal, + Unmarshaller: collectionUnmarshal, + Type: "collection", + } } } - if field.Input.Type == "" { - field.Input.Type = kind + return &MarshallerResult{ + Marshaller: defaultMarshal, + Unmarshaller: defaultUnmarshal, + Type: "text", } +} - if field.Marshaller == nil { - field.Marshaller = marshaller +func configure(field *FormField, form *Form) { + + if form.Data != nil && field.InitialValue == nil { + field.reflect = form.reflect.FieldByName(field.Name) } - if field.Unmarshaller == nil { - field.Unmarshaller = unmarshaller + if field.reflect.Kind() == reflect.Invalid && field.InitialValue != nil { + + field.reflect = reflect.ValueOf(field.InitialValue) } -} -func marshal(field *FormField, form *Form) { - field.Marshaller(field, form) -} + if field.reflect.Kind() != reflect.Invalid { + field.InitialValue = field.reflect.Interface() + } -func unmarshal(field *FormField, form *Form, values url.Values) { - field.Errors = []string{} + result := findMarshaller(field.reflect) - if values.Has(field.Input.Name) { - field.Input.Value = values.Get(field.Input.Name) + if field.Input.Type == "" { + field.Input.Type = result.Type } - field.Unmarshaller(field, form, values) + if field.Marshaller == nil { + field.Marshaller = result.Marshaller + } - field.HasErrors = len(field.Errors) > 0 + if field.Unmarshaller == nil { + field.Unmarshaller = result.Unmarshaller + } } func defaultMarshal(field *FormField, form *Form) error { @@ -144,7 +176,9 @@ func defaultUnmarshal(field *FormField, form *Form, values url.Values) error { } func booleanMarshal(field *FormField, form *Form) error { - defaultMarshal(field, form) + if err := defaultMarshal(field, form); err != nil { + return err + } if v, ok := BoolToStr(field.InitialValue); ok && !field.HasErrors { field.Input.Value = v @@ -154,7 +188,9 @@ func booleanMarshal(field *FormField, form *Form) error { } func booleanUnmarshal(field *FormField, form *Form, values url.Values) error { - defaultUnmarshal(field, form, values) + if err := defaultUnmarshal(field, form, values); err != nil { + return err + } if v, ok := StrToBool(field.SubmittedValue); ok && !field.HasErrors { field.SubmittedValue = v @@ -164,7 +200,9 @@ func booleanUnmarshal(field *FormField, form *Form, values url.Values) error { } func numberMarshal(field *FormField, form *Form) error { - defaultMarshal(field, form) + if err := defaultMarshal(field, form); err != nil { + return err + } if field.InitialValue != nil { v, _ := NumberToStr(field.InitialValue) @@ -177,28 +215,23 @@ func numberMarshal(field *FormField, form *Form) error { } func numberUnmarshal(field *FormField, form *Form, values url.Values) error { - defaultUnmarshal(field, form, values) + if err := defaultUnmarshal(field, form, values); err != nil { + return err + } if field.HasErrors { return nil } - var rv reflect.Value var v string var ok bool - if field.reflectValue.IsValid() { - rv = field.reflectValue - } else { - rv = field.reflectField - } - if v, ok = field.SubmittedValue.(string); !ok { field.Errors = append(field.Errors, ErrInvalidType.Error()) return nil } - if value, ok := StrToNumber(v, rv.Kind()); ok { + if value, ok := StrToNumber(v, field.reflect.Kind()); ok { field.SubmittedValue = value } else { field.Errors = append(field.Errors, ErrInvalidType.Error()) @@ -212,7 +245,9 @@ func numberUnmarshal(field *FormField, form *Form, values url.Values) error { // but the parsed value is always formatted yyyy-mm-dd. // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/date func dateMarshal(field *FormField, form *Form) error { - defaultMarshal(field, form) + if err := defaultMarshal(field, form); err != nil { + return err + } if v, ok := field.InitialValue.(time.Time); ok { field.Input.Value = v.Format("2006-01-02") @@ -224,7 +259,9 @@ func dateMarshal(field *FormField, form *Form) error { } func dateUnmarshal(field *FormField, form *Form, values url.Values) error { - defaultUnmarshal(field, form, values) + if err := defaultUnmarshal(field, form, values); err != nil { + return err + } if v, ok := field.SubmittedValue.(string); ok { if t, err := time.ParseInLocation("2006-01-02", v, time.UTC); err != nil { @@ -264,7 +301,7 @@ func formUnmarshal(field *FormField, form *Form, values url.Values) error { } func collectionMarshal(field *FormField, form *Form) error { - options := field.InitialValue.(*FieldCollectionOptions) + options := field.Options.(*FieldCollectionOptions) field.Input.Name = fmt.Sprintf("%s%s", field.Prefix, field.Name) field.Input.Id = replacers.Replace(field.Input.Name) @@ -274,6 +311,7 @@ func collectionMarshal(field *FormField, form *Form) error { subField := create(value.Key, "form", subForm) subField.Input.Name = fmt.Sprintf("%s[%s]", field.Input.Name, value.Key) + subField.Input.Id = replacers.Replace(subField.Input.Name) subField.Prefix = field.Input.Name + "." @@ -287,7 +325,7 @@ func collectionMarshal(field *FormField, form *Form) error { } func collectionUnmarshal(field *FormField, form *Form, values url.Values) error { - options := field.InitialValue.(*FieldCollectionOptions) + options := field.Options.(*FieldCollectionOptions) for _, value := range options.Items { subField := field.Get(value.Key) @@ -302,7 +340,7 @@ func checkboxMarshal(field *FormField, form *Form) error { field.Input.Name = fmt.Sprintf("%s%s", field.Prefix, field.Name) field.Input.Id = replacers.Replace(field.Input.Name) - for i, option := range field.InitialValue.(FieldOptions) { + for i, option := range field.Options.(FieldOptions) { // find a nice way to generate the name subField := CreateFormField() subField.Name = fmt.Sprintf("%d", i) @@ -321,7 +359,7 @@ func checkboxMarshal(field *FormField, form *Form) error { func checkboxUnmarshal(field *FormField, form *Form, values url.Values) error { // we need to check for extra values! submitedValues := FieldOptions{} - for i, option := range field.InitialValue.(FieldOptions) { + for i, option := range field.Options.(FieldOptions) { name := fmt.Sprintf("%d", i) value, err := getValue(field.Get(name), values) @@ -345,3 +383,40 @@ func checkboxUnmarshal(field *FormField, form *Form, values url.Values) error { return nil } + +func selectMarshal(field *FormField, form *Form) error { + field.Input.Name = fmt.Sprintf("%s%s", field.Prefix, field.Name) + field.Input.Id = replacers.Replace(field.Input.Name) + + for i, option := range field.Options.(FieldOptions) { + marshallers := findMarshaller(reflect.ValueOf(option.Value)) + + // find a nice way to generate the name + subField := CreateFormField() + subField.Name = fmt.Sprintf("%d", i) + subField.Input.Name = fmt.Sprintf("%s[%s]", field.Input.Name, subField.Name) + subField.Input.Id = replacers.Replace(subField.Input.Name) + subField.Label.Value = option.Label + subField.Input.Type = "option" + subField.Marshaller = marshallers.Marshaller + subField.Unmarshaller = marshallers.Unmarshaller + + marshal(subField, form) + + field.Children = append(field.Children, subField) + } + + return nil +} + +func selectUnmarshal(field *FormField, form *Form, values url.Values) error { + + if len(field.Children) == 0 { + field.Errors = append(field.Errors, "Unable to find any options") + field.HasErrors = true + } + + field.Children[0].Unmarshaller(field, form, values) + + return nil +} diff --git a/core/form/form_structs.go b/core/form/form_structs.go index 6c31e8b..d9f3966 100644 --- a/core/form/form_structs.go +++ b/core/form/form_structs.go @@ -110,9 +110,8 @@ type FormField struct { // from serialized to go Unmarshaller Unmarshaller // Validator - reflectValue reflect.Value - reflectField reflect.Value - Options interface{} + reflect reflect.Value + Options interface{} } func (f *FormField) Get(name string) *FormField { @@ -202,16 +201,9 @@ func create(name string, options ...interface{}) *FormField { if len(options) > 1 { field.InitialValue = options[1] - // try to read those cases from a registry, hardcoded for now. - // if v, ok := options[1].(FieldOptions); ok { - // field.Options = v - // } else { - // field.InitialValue = options[1] - // } } if len(options) > 2 { - field.InitialValue = options[1] field.Options = options[2] } @@ -231,15 +223,20 @@ func create(name string, options ...interface{}) *FormField { field.Unmarshaller = checkboxUnmarshal } + if field.Input.Type == "select" { + field.Marshaller = selectMarshal + field.Unmarshaller = selectUnmarshal + } + // if fieldType == "form" { // field.Marshaller = formMarshal // field.Unmarshaller = formUnmarshal // } - // if fieldType == "collection" { - // field.Marshaller = collectionMarshal - // field.Unmarshaller = collectionUnmarshal - // } + if field.Input.Type == "collection" { + field.Marshaller = collectionMarshal + field.Unmarshaller = collectionUnmarshal + } if field.Input.Type == "boolean" { // field.Marshaller = booleanMarshal @@ -593,17 +590,17 @@ func attachValues(fields []*FormField) { continue } - if field.reflectField.Kind() == reflect.Invalid { + if field.reflect.Kind() == reflect.Invalid { // fmt.Printf("attachValues > Invalid type: %s\n", field.Name) continue } newValue := reflect.ValueOf(field.SubmittedValue) - if newValue.CanConvert(field.reflectField.Type()) { - field.reflectField.Set(newValue.Convert(field.reflectField.Type())) + if newValue.CanConvert(field.reflect.Type()) { + field.reflect.Set(newValue.Convert(field.reflect.Type())) } else { - // fmt.Printf("attachValues > Unable to convert type: Type, field: %s (kind: %s) submitted: %s, value: %s\n", field.Name, field.reflectField.Kind(), newValue.Kind(), newValue.Interface()) + // fmt.Printf("attachValues > Unable to convert type: Type, field: %s (kind: %s) submitted: %s, value: %s\n", field.Name, field.reflect.Kind(), newValue.Kind(), newValue.Interface()) } } } diff --git a/core/form/form_structs_test.go b/core/form/form_structs_test.go index 5f04dc5..6d51d5e 100644 --- a/core/form/form_structs_test.go +++ b/core/form/form_structs_test.go @@ -19,7 +19,7 @@ type TestUser struct { Enabled bool Hidden bool Email string - Position int8 + Position int32 Ratio float32 DOB time.Time } @@ -234,7 +234,7 @@ func Test_Reflect(t *testing.T) { func Test_Bind_Form_Nested_Basic(t *testing.T) { form := CreateForm(nil) form.Add("name", "text", "John Doe") - form.Add("options", "checkbox", FieldOptions{ + form.Add("options", "checkbox", nil, FieldOptions{ {Label: "Enabled", Checked: true, Value: "enabled"}, {Label: "Hidden", Checked: false, Value: "hidden"}, }) @@ -242,7 +242,7 @@ func Test_Bind_Form_Nested_Basic(t *testing.T) { subForm := CreateForm(nil) subForm.Add("title", "text", "The title") subForm.Add("Body", "text", "The body") - subForm.Add("options", "checkbox", FieldOptions{ + subForm.Add("options", "checkbox", nil, FieldOptions{ {Label: "Is Validated", Checked: true, Value: "validated"}, }) @@ -311,7 +311,7 @@ func Test_Bind_Form_Nested_Basic_Struct(t *testing.T) { subForm.Add("Title", "text") subForm.Add("Body", "text") subForm.Add("IsValidated", "boolean") - subForm.Add("options", "checkbox", FieldOptions{ + subForm.Add("options", "checkbox", nil, FieldOptions{ {Label: "Enabled", Checked: true, Value: "enabled"}, {Label: "Hidden", Checked: false, Value: "hidden"}, }) @@ -366,7 +366,7 @@ func Test_Bind_Form_Nested_Basic_Struct(t *testing.T) { assert.Equal(t, "Thomas", user.Name) assert.Equal(t, false, user.Enabled) assert.Equal(t, float32(1.2), user.Ratio) - assert.Equal(t, int8(12), user.Position) + assert.Equal(t, int32(12), user.Position) assert.Equal(t, "New title", blog.Title) assert.Equal(t, true, blog.IsValidated) // not submitted } @@ -377,15 +377,15 @@ func Test_Bind_Form_Collection(t *testing.T) { {Key: "1", Value: &TestTag{Id: 1, Name: "tag2", Enabled: true}}, } - form := &Form{} - form.Add("tags", "collection", &FieldCollectionOptions{ + form := CreateForm(nil) + form.Add("tags", "collection", nil, &FieldCollectionOptions{ Items: values, Configure: func(value interface{}) *Form { tag := value.(*TestTag) - tagForm := &Form{} + tagForm := CreateForm(nil) tagForm.Add("name", "text", tag.Name) - tagForm.Add("options", "checkbox", FieldOptions{ + tagForm.Add("options", "checkbox", nil, FieldOptions{ {Label: "Is Enabled", Checked: tag.Enabled, Value: "enabled"}, }) @@ -415,27 +415,33 @@ func Test_Bind_Form_Select(t *testing.T) { Name: "John Doe", Enabled: true, Hidden: false, - Position: 1, + Position: int32(1), } form := CreateForm(user) - form.Add("Enabled", "select", FieldOptions{ - {Label: "Yes", Value: true}, + form.Add("Enabled", "select", nil, FieldOptions{ {Label: "No", Value: false}, + {Label: "Yes", Value: true}, }) - form.Add("Position", "select", FieldOptions{ - {Label: "1", Value: 1}, - {Label: "2", Value: 2}, - {Label: "3", Value: 3}, - {Label: "4", Value: 4}, + form.Add("Position", "select", nil, FieldOptions{ + {Label: "1", Value: int32(1)}, + {Label: "2", Value: int32(2)}, + {Label: "3", Value: int32(3)}, + {Label: "4", Value: int32(4)}, }) PrepareForm(form) - // .SetOptions(FieldOptions{ - // {Label: "Yes", Value: true}, - // {Label: "No", Value: false}, - // }) + v := url.Values{ + "Enabled": []string{"0"}, + "Position": []string{"3"}, + } + + BindUrlValues(form, v) + + AttachValues(form) + assert.Equal(t, false, user.Enabled) + assert.Equal(t, int32(3), user.Position) }