package form // @license GNU AGPL version 3 or any later version // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as // published by the Free Software Foundation, either version 3 of the // License, or (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . import ( "fmt" "maps" "slices" "strings" "gitnet.fr/deblan/go-form/util" "gitnet.fr/deblan/go-form/validation" ) // Generic function for field.Validation func DefaultFieldValidation(f *Field) bool { if len(f.Children) > 0 { isValid := true for _, c := range f.Children { c.ResetErrors() isChildValid, errs := validation.Validate(c.Data, c.Constraints) if len(errs) > 0 { c.Errors = errs } isValid = isChildValid && isValid for _, sc := range c.Children { isValid = DefaultFieldValidation(sc) && isValid } } return isValid } else { f.ResetErrors() isValid, errs := validation.Validate(f.Data, f.Constraints) if len(errs) > 0 { f.Errors = errs } return isValid } } // Field represents a field in a form type Field struct { Name string `json:"name"` NamePrefix string `json:"name_prefix"` Widget string `json:"widget"` Data any `json:"-"` Options []*Option `json:"options"` Children []*Field `json:"children"` Constraints []validation.Constraint `json:"-"` Errors []validation.Error `json:"-"` BeforeMount func(data any) (any, error) `json:"-"` BeforeBind func(data any) (any, error) `json:"-"` Validate func(f *Field) bool `json:"-"` IsSlice bool `json:"is_slice"` IsCollection bool `json:"is_collection"` IsFixedName bool `json:"is_fixed_name"` Form *Form `json:"-"` Parent *Field `json:"-"` } // Generates a new field with default properties // It should not be used directly but inside function like in form.NewFieldText func NewField(name, widget string) *Field { f := &Field{ Name: name, IsFixedName: false, Widget: widget, Data: nil, } f.BeforeMount = func(data any) (any, error) { return data, nil } f.BeforeBind = func(data any) (any, error) { return data, nil } f.WithOptions( NewOption("attr", Attrs{}), NewOption("row_attr", Attrs{}), NewOption("label_attr", Attrs{}), NewOption("help_attr", Attrs{}), ) f.Validate = DefaultFieldValidation return f } func (f *Field) Copy() *Field { return &Field{ Name: f.Name, Form: f.Form, Widget: f.Widget, Options: f.Options, Constraints: f.Constraints, BeforeMount: f.BeforeMount, BeforeBind: f.BeforeBind, Validate: f.Validate, IsSlice: f.IsSlice, IsFixedName: f.IsFixedName, } } // Checks if the field contains an option using its name func (f *Field) HasOption(name string) bool { for _, option := range f.Options { if option.Name == name { return true } } return false } // Returns an option using its name func (f *Field) GetOption(name string) *Option { for _, option := range f.Options { if option.Name == name { return option } } return nil } // Appends options to the field func (f *Field) WithOptions(options ...*Option) *Field { for _, option := range options { if f.HasOption(option.Name) { f.GetOption(option.Name).Value = option.Value } else { f.Options = append(f.Options, option) } } return f } // Remove an option if exists func (f *Field) RemoveOption(name string) *Field { var options []*Option for _, option := range f.Options { if option.Name != name { options = append(options, option) } } f.Options = options return f } // Sets data the field func (f *Field) WithData(data any) *Field { f.Data = data return f } // Resets the field errors func (f *Field) ResetErrors() *Field { f.Errors = []validation.Error{} return f } // Sets that the field represents a data slice func (f *Field) WithSlice() *Field { f.IsSlice = true return f } // Sets that the field represents a collection func (f *Field) WithCollection() *Field { f.IsCollection = true return f } // Sets that the name of the field is not computed func (f *Field) WithFixedName() *Field { f.IsFixedName = true return f } // Appends constraints func (f *Field) WithConstraints(constraints ...validation.Constraint) *Field { for _, constraint := range constraints { f.Constraints = append(f.Constraints, constraint) } return f } // Sets a transformer applied to the structure data before displaying it in a field func (f *Field) WithBeforeMount(callback func(data any) (any, error)) *Field { f.BeforeMount = callback return f } // Sets a transformer applied to the data of a field before defining it in a structure func (f *Field) WithBeforeBind(callback func(data any) (any, error)) *Field { f.BeforeBind = callback return f } // Appends children func (f *Field) Add(children ...*Field) *Field { for _, child := range children { child.Parent = f f.Children = append(f.Children, child) } return f } // Checks if the field contains a child using its name func (f *Field) HasChild(name string) bool { for _, child := range f.Children { if name == child.Name { return true } } return false } // Returns a child using its name func (f *Field) GetChild(name string) *Field { var result *Field for _, child := range f.Children { if name == child.Name { result = child break } } return result } // Computes the name of the field func (f *Field) GetName() string { var name string if f.IsFixedName { return f.Name } if f.Form != nil && f.Form.Name != "" { name = fmt.Sprintf("%s%s[%s]", f.Form.Name, f.NamePrefix, f.Name) } else if f.Parent != nil { name = fmt.Sprintf("%s%s[%s]", f.Parent.GetName(), f.NamePrefix, f.Name) } else { name = f.Name } return name } // Computes the id of the field func (f *Field) GetId() string { name := f.GetName() name = strings.ReplaceAll(name, "[", "-") name = strings.ReplaceAll(name, "]", "") name = strings.ToLower(name) return name } // Populates the field with data func (f *Field) Mount(data any) error { data, err := f.BeforeMount(data) if err != nil { return err } if len(f.Children) == 0 { f.Data = data return nil } props, err := util.InspectStruct(data) if err != nil { return err } for key, value := range props { if f.HasChild(key) { err = f.GetChild(key).Mount(value) if err != nil { return err } } } return nil } // Bind the data into the given map func (f *Field) Bind(data map[string]any, key *string, parentIsSlice bool) error { if len(f.Children) == 0 { v, err := f.BeforeBind(f.Data) if err != nil { return err } if key != nil { data[*key] = v } else { data[f.Name] = v } return nil } data[f.Name] = make(map[string]any) for _, child := range f.Children { child.Bind(data[f.Name].(map[string]any), key, f.IsCollection) } if f.IsCollection { var nextData []any values := data[f.Name].(map[string]any) var keys []string for key, _ := range values { keys = append(keys, key) } slices.Sort(keys) for _, key := range keys { for valueKey, value := range values { if valueKey == key { nextData = append(nextData, value) } } } data[f.Name] = nextData } return nil } // Generates a tree of errors func (f *Field) ErrorsTree(tree map[string]any, key *string) { var index string if key != nil { index = *key } else { index = f.Name } if len(f.Children) == 0 { if len(f.Errors) > 0 { tree[index] = map[string]any{ "meta": map[string]any{ "id": f.GetId(), "name": f.Name, "formName": f.GetName(), }, "errors": f.Errors, } } } else { errors := make(map[string]any) for _, child := range f.Children { if len(child.Errors) > 0 { child.ErrorsTree(errors, &child.Name) } } if len(errors) > 0 { tree[index] = map[string]any{ "meta": map[string]any{ "id": f.GetId(), "name": f.Name, "formName": f.GetName(), }, "errors": []validation.Error{}, "children": slices.Collect(maps.Values(errors)), } } } }