commit d735f6e4728efe92eb087b8837c15eb9dc876b68 Author: Simon Vieille Date: Wed Jul 16 16:43:26 2025 +0200 init diff --git a/form/field.go b/form/field.go new file mode 100644 index 0000000..540de16 --- /dev/null +++ b/form/field.go @@ -0,0 +1,172 @@ +package form + +import ( + "gitnet.fr/deblan/go-form/util" + "gitnet.fr/deblan/go-form/validation" +) + +func FieldValidation(f *Field) bool { + if len(f.Children) > 0 { + isValid := true + + for _, c := range f.Children { + isChildValid, errs := validation.Validate(c.Data, c.Constraints) + + if len(errs) > 0 { + c.Errors = errs + } + + isValid = isValid && isChildValid + } + + return isValid + } else { + isValid, errs := validation.Validate(f.Data, f.Constraints) + f.Errors = []validation.Error{} + + if len(errs) > 0 { + f.Errors = errs + } + + return isValid + } +} + +type Field struct { + Name string + Widget string + Data any + Options []*Option + Children []*Field + Constraints []validation.Constraint + Errors []validation.Error + PrepareView func() map[string]any + BeforeBind func(data any) (any, error) + Validate func(f *Field) bool +} + +func NewField(name, widget string) *Field { + f := &Field{ + Name: name, + Widget: widget, + Data: nil, + } + + f.PrepareView = func() map[string]any { + m := make(map[string]any) + + return m + } + + f.BeforeBind = func(data any) (any, error) { + return data, nil + } + + f.Validate = FieldValidation + + return f +} + +func (f *Field) HasOption(name string) bool { + for _, option := range f.Options { + if option.Name == name { + return true + } + } + + return false +} + +func (f *Field) GetOption(name string) *Option { + for _, option := range f.Options { + if option.Name == name { + return option + } + } + + return nil +} + +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 +} + +func (f *Field) WithConstraints(constraints ...validation.Constraint) *Field { + for _, constraint := range constraints { + f.Constraints = append(f.Constraints, constraint) + } + + return f +} + +func (f *Field) Add(children ...*Field) *Field { + for _, child := range children { + f.Children = append(f.Children, child) + } + + return f +} + +func (f *Field) HasChild(name string) bool { + for _, child := range f.Children { + if name == child.Name { + return true + } + } + + return false +} + +func (f *Field) GetChild(name string) *Field { + var result *Field + + for _, child := range f.Children { + if name == child.Name { + result = child + + break + } + } + + return result +} + +func (f *Field) Bind(data any) error { + if len(f.Children) == 0 { + f.Data = data + + return nil + } + + data, err := f.BeforeBind(data) + + if err != nil { + return err + } + + props, err := util.InspectStruct(data) + + if err != nil { + return err + } + + for key, value := range props { + if f.HasChild(key) { + err = f.GetChild(key).Bind(value) + + if err != nil { + return err + } + } + } + + return nil +} diff --git a/form/field_input.go b/form/field_input.go new file mode 100644 index 0000000..dc7206f --- /dev/null +++ b/form/field_input.go @@ -0,0 +1,8 @@ +package form + +func NewFieldText(name string) *Field { + f := NewField(name, "input"). + WithOptions(Option{Name: "type", Value: "text"}) + + return f +} diff --git a/form/field_subform.go b/form/field_subform.go new file mode 100644 index 0000000..9c5f21c --- /dev/null +++ b/form/field_subform.go @@ -0,0 +1,11 @@ +package form + +func NewFieldSubForm(name string) *Field { + f := NewField(name, "sub_form") + + return f +} + +func NewSubForm(name string) *Field { + return NewFieldSubForm(name) +} diff --git a/form/form.go b/form/form.go new file mode 100644 index 0000000..9cbf600 --- /dev/null +++ b/form/form.go @@ -0,0 +1,117 @@ +package form + +import ( + "net/http" + + "gitnet.fr/deblan/go-form/util" + "gitnet.fr/deblan/go-form/validation" +) + +type Form struct { + Fields []*Field + Errors []validation.Error + Method string + Action string + Name string + Options []Option +} + +func NewForm(fields ...*Field) *Form { + f := new(Form) + f.Method = "POST" + f.Add(fields...) + + return f +} + +func (f *Form) Add(fields ...*Field) { + for _, field := range fields { + f.Fields = append(f.Fields, field) + } +} + +func (f *Form) HasField(name string) bool { + for _, field := range f.Fields { + if name == field.Name { + return true + } + } + + return false +} + +func (f *Form) GetField(name string) *Field { + var result *Field + + for _, field := range f.Fields { + if name == field.Name { + result = field + break + } + } + + return result +} + +func (f *Form) WithMethod(v string) *Form { + f.Method = v + + return f +} + +func (f *Form) WithName(v string) *Form { + f.Name = v + + return f +} + +func (f *Form) WithAction(v string) *Form { + f.Action = v + + return f +} + +func (f *Form) WithOptions(options ...Option) *Form { + for _, option := range options { + f.Options = append(f.Options, option) + } + + return f +} + +func (f *Form) IsValid() bool { + isValid := true + + for _, field := range f.Fields { + fieldIsValid := field.Validate(field) + isValid = isValid && fieldIsValid + } + + return isValid +} + +func (f *Form) Bind(data any) error { + props, err := util.InspectStruct(data) + + if err != nil { + return err + } + + for key, value := range props { + if f.HasField(key) { + err = f.GetField(key).Bind(value) + + if err != nil { + return err + } + } + } + + return nil +} + +func (f *Form) HandleRequest(req http.Request) { + if f.Method == "POST" { + // data := req.PostForm + } +} diff --git a/form/option.go b/form/option.go new file mode 100644 index 0000000..c811db1 --- /dev/null +++ b/form/option.go @@ -0,0 +1,6 @@ +package form + +type Option struct { + Name string + Value any +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..f28e114 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module gitnet.fr/deblan/go-form + +go 1.23.0 diff --git a/main.go b/main.go new file mode 100644 index 0000000..71d87b2 --- /dev/null +++ b/main.go @@ -0,0 +1,77 @@ +package main + +import ( + "fmt" + + "gitnet.fr/deblan/go-form/form" + "gitnet.fr/deblan/go-form/theme" + "gitnet.fr/deblan/go-form/validation" +) + +func main() { + type Address struct { + Street string + City string + ZipCode uint + } + + type Person struct { + Name string + Address Address + } + + data := new(Person) + data.Name = "" + data.Address = Address{ + Street: "rue des camélias", + City: "", + ZipCode: 39700, + } + + f := form.NewForm( + form.NewFieldText("Name"). + WithOptions( + form.Option{Name: "required", Value: true}, + ). + WithConstraints( + validation.NotBlank{}, + ), + form.NewSubForm("Address"). + Add( + form.NewFieldText("Street"), + form.NewFieldText("City"). + WithConstraints( + validation.NotBlank{}, + ), + form.NewFieldText("ZipCode"), + ), + ).WithMethod("POST").WithAction("") + + f.Bind(data) + + fmt.Printf("%+v\n", f.IsValid()) + + render := theme.NewRenderer(theme.Html5) + v := render.RenderForm(f) + + fmt.Print(v) + + // r, e := theme.RenderForm(f, theme.Html5) + + // fmt.Printf("%+v\n", e) + // fmt.Printf("%+v\n", r) + + // fmt.Printf("%+v\n", e) + // + // fmt.Printf("%+v\n", f) + // + // for _, field := range f.Fields { + // fmt.Printf("%+v\n", *field) + // + // if len(field.Children) > 0 { + // for _, c := range field.Children { + // fmt.Printf("C %+v\n", *c) + // } + // } + // } +} diff --git a/theme/html5.go b/theme/html5.go new file mode 100644 index 0000000..9b50f39 --- /dev/null +++ b/theme/html5.go @@ -0,0 +1,27 @@ +package theme + +var Html5 = map[string]string{ + "form": `
+ {{ form_error .Form nil }} + {{ .Content }} +
`, + "label": ``, + "input": ``, + "sub_form": ` + {{ form_label .Field }} + + {{ range $field := .Field.Children }} + {{ form_row $field }} + {{ end }} + `, + "error": `
+ {{ range $error := .Errors }} + {{ $error }}
+ {{ end }} +
`, + "row": `
+ {{ form_label .Field }} + {{ form_error nil .Field }} + {{ form_widget .Field }} +
`, +} diff --git a/theme/renderer.go b/theme/renderer.go new file mode 100644 index 0000000..a0fc09c --- /dev/null +++ b/theme/renderer.go @@ -0,0 +1,90 @@ +package theme + +import ( + "bytes" + "html/template" + + "gitnet.fr/deblan/go-form/form" + "gitnet.fr/deblan/go-form/validation" +) + +type Renderer struct { + Theme map[string]string +} + +func NewRenderer(theme map[string]string) *Renderer { + r := new(Renderer) + r.Theme = theme + + return r +} + +func (r *Renderer) RenderForm(form *form.Form) template.HTML { + content := "" + + for _, field := range form.Fields { + content = content + string(r.RenderRow(field)) + } + + return r.Render("form", r.Theme["form"], map[string]any{ + "Form": form, + "Content": template.HTML(content), + }) +} + +func (r *Renderer) RenderRow(field *form.Field) template.HTML { + return r.Render("row", r.Theme["row"], map[string]any{ + "Field": field, + }) +} + +func (r *Renderer) RenderLabel(field *form.Field) template.HTML { + return r.Render("label", r.Theme["label"], map[string]any{ + "Field": field, + }) +} + +func (r *Renderer) RenderWidget(field *form.Field) template.HTML { + return r.Render("widget", r.Theme[field.Widget], map[string]any{ + "Field": field, + }) +} + +func (r *Renderer) RenderError(form *form.Form, field *form.Field) template.HTML { + var errors []validation.Error + + if field != nil { + errors = field.Errors + } + + if form != nil { + errors = form.Errors + } + + return r.Render("error", r.Theme["error"], map[string]any{ + "Errors": errors, + }) +} + +func (r *Renderer) Render(name, tpl string, args any) template.HTML { + t, err := template.New(name).Funcs(template.FuncMap{ + "form": r.RenderForm, + "form_row": r.RenderRow, + "form_label": r.RenderLabel, + "form_widget": r.RenderWidget, + "form_error": r.RenderError, + }).Parse(tpl) + + if err != nil { + return template.HTML(err.Error()) + } + + var buf bytes.Buffer + err = t.Execute(&buf, args) + + if err != nil { + return template.HTML(err.Error()) + } + + return template.HTML(buf.String()) +} diff --git a/util/inspect.go b/util/inspect.go new file mode 100644 index 0000000..6cb1bf7 --- /dev/null +++ b/util/inspect.go @@ -0,0 +1,30 @@ +package util + +import ( + "errors" + "reflect" +) + +func InspectStruct(input interface{}) (map[string]interface{}, error) { + val := reflect.ValueOf(input) + + if val.Kind() == reflect.Ptr { + val = val.Elem() + } + + if val.Kind() != reflect.Struct { + return nil, errors.New("Invalid type") + } + + result := make(map[string]interface{}) + + typ := val.Type() + for i := 0; i < val.NumField(); i++ { + field := typ.Field(i) + value := val.Field(i) + + result[field.Name] = value.Interface() + } + + return result, nil +} diff --git a/validation/constraint.go b/validation/constraint.go new file mode 100644 index 0000000..5c42183 --- /dev/null +++ b/validation/constraint.go @@ -0,0 +1,5 @@ +package validation + +type Constraint interface { + Validate(data any) []Error +} diff --git a/validation/error.go b/validation/error.go new file mode 100644 index 0000000..078a221 --- /dev/null +++ b/validation/error.go @@ -0,0 +1,3 @@ +package validation + +type Error string diff --git a/validation/notblank.go b/validation/notblank.go new file mode 100644 index 0000000..250ea31 --- /dev/null +++ b/validation/notblank.go @@ -0,0 +1,46 @@ +package validation + +import ( + "fmt" + "reflect" +) + +type NotBlank struct { +} + +func (c NotBlank) Validate(data any) []Error { + isValid := true + errors := []Error{} + + fmt.Printf("%+v\n", data) + + t := reflect.TypeOf(data) + + if t.Kind() == reflect.Ptr { + t = t.Elem() + } + + if data == nil { + isValid = false + } else if t.Kind() == reflect.Bool { + if data == false { + isValid = false + } + } else if t.Kind() == reflect.Array { + if len(data.([]interface{})) == 0 { + isValid = false + } + } else if t.Kind() == reflect.String { + if len(data.(string)) == 0 { + isValid = false + } + } else { + fmt.Printf("d=%+v\n", data) + } + + if !isValid { + errors = append(errors, Error("This value should be blank")) + } + + return errors +} diff --git a/validation/validation.go b/validation/validation.go new file mode 100644 index 0000000..b824d14 --- /dev/null +++ b/validation/validation.go @@ -0,0 +1,13 @@ +package validation + +func Validate(data any, constraints []Constraint) (bool, []Error) { + errs := []Error{} + + for _, constraint := range constraints { + for _, e := range constraint.Validate(data) { + errs = append(errs, e) + } + } + + return len(errs) == 0, errs +}