From ca9d6a543335e998963ac4f680cf5c47e597602b Mon Sep 17 00:00:00 2001 From: René 'Necoro' Neumann Date: Wed, 16 Oct 2024 22:18:06 +0200 Subject: Inline form package --- form.go | 42 --------------- form/builder.go | 96 +++++++++++++++++++++++++++++++++++ form/decode.go | 29 +++++++++++ form/reflect.go | 149 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ templ/template.go | 8 ++- 5 files changed, 277 insertions(+), 47 deletions(-) delete mode 100644 form.go create mode 100644 form/builder.go create mode 100644 form/decode.go create mode 100644 form/reflect.go diff --git a/form.go b/form.go deleted file mode 100644 index db0d097..0000000 --- a/form.go +++ /dev/null @@ -1,42 +0,0 @@ -package main - -import ( - "fmt" - "log" - "net/http" - - "github.com/gorilla/schema" -) - -var schemaDecoder *schema.Decoder - -func init() { - schemaDecoder = schema.NewDecoder() - schemaDecoder.IgnoreUnknownKeys(true) -} - -type fieldError struct { - Field string - Issue string -} - -func (fe fieldError) Error() string { - return fmt.Sprintf("%s: %v", fe.Field, fe.Issue) -} - -func (fe fieldError) FieldError() (field, err string) { - return fe.Field, fe.Issue -} - -func parseForm[T any](r *http.Request, data *T) { - if err := r.ParseForm(); err != nil { - log.Panic("Parsing form: ", err) - } - if err := schemaDecoder.Decode(data, r.PostForm); err != nil { - log.Panic("Decoding form: ", err) - } - - if withCsrf, ok := any(data).(WithCsrf); ok { - withCsrf.SetCsrfField(r) - } -} diff --git a/form/builder.go b/form/builder.go new file mode 100644 index 0000000..97e972f --- /dev/null +++ b/form/builder.go @@ -0,0 +1,96 @@ +package form + +import ( + "errors" + "fmt" + "html/template" + "strings" +) + +type builder struct { + tpl *template.Template +} + +// Inputs will parse the provided struct into fields and then execute the +// template with each field. The returned HTML is simply all of these results +// appended one after another. +// +// Inputs' second argument - errs - will be used to render errors for +// individual fields. +func (b *builder) Inputs(v interface{}, errs ...error) (template.HTML, error) { + tpl, err := b.tpl.Clone() + if err != nil { + return "", err + } + fields := fields(v) + errors := fieldErrors(errs) + var html template.HTML + for _, field := range fields { + var sb strings.Builder + tpl.Funcs(template.FuncMap{ + "errors": func() []string { + if errs, ok := errors[field.Name]; ok { + return errs + } + return nil + }, + }) + err := tpl.Execute(&sb, field) + if err != nil { + return "", err + } + html = html + template.HTML(sb.String()) + } + return html, nil +} + +// FuncMap returns a template.FuncMap that defines both the inputs_for and +// inputs_and_errors_for functions for usage in the template package. The +// latter is provided via a closure because variadic parameters and the +// template package don't play very nicely and this just simplifies things +// a lot for end users of the form package. +func FuncMap(formTpl *template.Template) template.FuncMap { + b := builder{tpl: formTpl} + return template.FuncMap{ + "inputs_for": b.Inputs, + "inputs_and_errors_for": func(v interface{}, errs []error) (template.HTML, error) { + return b.Inputs(v, errs...) + }, + } +} + +// ParsingFuncMap is present to make it a little easier to build the input template. +// In order to parse a template that uses the `errors` function, you need to have +// that template defined when the template is parsed. We clearly don't know whether +// a field has an error or not until it is parsed. +func ParsingFuncMap() template.FuncMap { + return template.FuncMap{ + "errors": func() []string { + return nil + }, + } +} + +type FieldError struct { + Field string + Issue string +} + +func (fe FieldError) Error() string { + return fmt.Sprintf("%s: %v", fe.Field, fe.Issue) +} + +// errors will build a map where each key is the field name, and each +// value is a slice of strings representing errors with that field. +func fieldErrors(errs []error) map[string][]string { + ret := make(map[string][]string) + for _, err := range errs { + var fe FieldError + if !errors.As(err, &fe) { + fmt.Println(err, "isnt field error") + continue + } + ret[fe.Field] = append(ret[fe.Field], fe.Issue) + } + return ret +} diff --git a/form/decode.go b/form/decode.go new file mode 100644 index 0000000..7ebdfc2 --- /dev/null +++ b/form/decode.go @@ -0,0 +1,29 @@ +package form + +import ( + "gosten/csrf" + "log" + "net/http" + + "github.com/gorilla/schema" +) + +var schemaDecoder *schema.Decoder + +func init() { + schemaDecoder = schema.NewDecoder() + schemaDecoder.IgnoreUnknownKeys(true) +} + +func Parse[T any](r *http.Request, data *T) { + if err := r.ParseForm(); err != nil { + log.Panic("Parsing form: ", err) + } + if err := schemaDecoder.Decode(data, r.PostForm); err != nil { + log.Panic("Decoding form: ", err) + } + + if withCsrf, ok := any(data).(csrf.Enabled); ok { + withCsrf.SetCsrfField(r) + } +} diff --git a/form/reflect.go b/form/reflect.go new file mode 100644 index 0000000..4dd4018 --- /dev/null +++ b/form/reflect.go @@ -0,0 +1,149 @@ +package form + +import ( + "html/template" + "reflect" + "strings" +) + +// valueOf is basically just reflect.ValueOf, but if the Kind() of the +// value is a pointer or interface it will try to get the reflect.Value +// of the underlying element, and if the pointer is nil it will +// create a new instance of the type and return the reflect.Value of it. +// +// This is used to make the rest of the fields function simpler. +func valueOf(v interface{}) reflect.Value { + rv := reflect.ValueOf(v) + // If a nil pointer is passed in but has a type we can recover, but I + // really should just panic and tell people to fix their shitty code. + if rv.Type().Kind() == reflect.Pointer && rv.IsNil() { + rv = reflect.Zero(rv.Type().Elem()) + } + // If we have a pointer or interface let's try to get the underlying + // element + for rv.Kind() == reflect.Pointer || rv.Kind() == reflect.Interface { + rv = rv.Elem() + } + return rv +} + +func fields(v interface{}, names ...string) []field { + rv := valueOf(v) + if rv.Kind() != reflect.Struct { + // We can't really do much with a non-struct type. I suppose this + // could eventually support maps as well, but for now it does not. + panic("invalid value; only structs are supported") + } + + t := rv.Type() + vFields := reflect.VisibleFields(t) + ret := make([]field, 0, len(vFields)) + for _, tf := range vFields { + if !tf.IsExported() { + continue + } + + rf := rv.FieldByIndex(tf.Index) + // If this is a nil pointer, create a new instance of the element. + if tf.Type.Kind() == reflect.Pointer && rf.IsNil() { + rf = reflect.Zero(tf.Type.Elem()) + } + + // If this is a struct it has nested fields we need to add. The + // simplest way to do this is to recursively call `fields` but + // to provide the name of this struct field to be added as a prefix + // to the fields. + // This does not apply to anonymous structs, because their fields are + // seen as "inlined". + if reflect.Indirect(rf).Kind() == reflect.Struct { + if !tf.Anonymous { + ret = append(ret, fields(rf.Interface(), append(names, tf.Name)...)...) + } + continue + } + + // If we are still in this loop then we aren't dealing with a nested + // struct and need to add the field. First we check to see if the + // ignore tag is present, then we set default values, then finally + // we overwrite defaults with any provided tags. + tags, ignored := parseTags(tf.Tag.Get("form")) + if ignored { + continue + } + name := append(names, tf.Name) + f := field{ + Name: strings.Join(name, "."), + Label: tf.Name, + Placeholder: tf.Name, + Type: "text", + Value: rf.Interface(), + } + f.applyTags(tags) + ret = append(ret, f) + } + return ret +} + +func (f *field) applyTags(tags map[string]string) { + if v, ok := tags["name"]; ok { + f.Name = v + } + if v, ok := tags["label"]; ok { + f.Label = v + // DO NOT move this label check after the placeholder check or + // this will cause issues. + f.Placeholder = v + } + if v, ok := tags["placeholder"]; ok { + f.Placeholder = v + } + if v, ok := tags["type"]; ok { + f.Type = v + } + if v, ok := tags["id"]; ok { + f.ID = v + } + if v, ok := tags["footer"]; ok { + // Probably shouldn't be HTML but whatever. + f.Footer = template.HTML(v) + } + if v, ok := tags["class"]; ok { + f.Class = v + } + if v, ok := tags["options"]; ok { + f.Options = strings.Split(v, ",") + } +} + +func parseTags(tags string) (map[string]string, bool) { + tags = strings.TrimSpace(tags) + if len(tags) == 0 { + return map[string]string{}, false + } + split := strings.Split(tags, ";") + ret := make(map[string]string, len(split)) + for _, tag := range split { + kv := strings.Split(tag, "=") + if len(kv) < 2 { + if kv[0] == "-" { + return nil, true + } + continue + } + k, v := strings.TrimSpace(kv[0]), strings.TrimSpace(kv[1]) + ret[k] = v + } + return ret, false +} + +type field struct { + Name string + Label string + Placeholder string + Type string + ID string + Options []string + Value interface{} + Footer template.HTML + Class string +} diff --git a/templ/template.go b/templ/template.go index f609bc4..9cefadc 100644 --- a/templ/template.go +++ b/templ/template.go @@ -7,7 +7,7 @@ import ( "os" "sync" - "github.com/Necoro/form" + "gosten/form" ) //go:embed *.tpl @@ -17,7 +17,6 @@ var templates = make(map[string]*template.Template) var muTpl sync.RWMutex var baseTpl *template.Template -var formBuilder form.Builder var isLive = sync.OnceValue(checkLive) @@ -31,10 +30,9 @@ func checkLive() bool { func loadBase(fs fs.FS) { baseTpl = template.Must(template.New("base.tpl"). - Funcs(form.FuncMap()). + Funcs(form.ParsingFuncMap()). ParseFS(fs, "base.tpl", "form.tpl", "navlinks.tpl", "content.tpl")) - formBuilder = form.Builder{InputTemplate: baseTpl.Lookup("formItem")} - baseTpl.Funcs(formBuilder.FuncMap()) + baseTpl.Funcs(form.FuncMap(baseTpl.Lookup("formItem"))) } func Lookup(name string) *template.Template { -- cgit v1.2.3-70-g09d2