summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRené 'Necoro' Neumann <necoro@necoro.eu>2024-10-16 22:18:06 +0200
committerRené 'Necoro' Neumann <necoro@necoro.eu>2024-10-16 22:18:06 +0200
commitca9d6a543335e998963ac4f680cf5c47e597602b (patch)
treecaaeb8009bc64e800bd1b4cbee592954e3f5b8f9
parentb20670125fede069a77e25d6414c3a5230e56f36 (diff)
downloadgosten-ca9d6a543335e998963ac4f680cf5c47e597602b.tar.gz
gosten-ca9d6a543335e998963ac4f680cf5c47e597602b.tar.bz2
gosten-ca9d6a543335e998963ac4f680cf5c47e597602b.zip
Inline form package
Diffstat (limited to '')
-rw-r--r--form/builder.go96
-rw-r--r--form/decode.go (renamed from form.go)21
-rw-r--r--form/reflect.go149
-rw-r--r--templ/template.go8
4 files changed, 252 insertions, 22 deletions
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.go b/form/decode.go
index db0d097..7ebdfc2 100644
--- a/form.go
+++ b/form/decode.go
@@ -1,7 +1,7 @@
-package main
+package form
import (
- "fmt"
+ "gosten/csrf"
"log"
"net/http"
@@ -15,20 +15,7 @@ func init() {
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) {
+func Parse[T any](r *http.Request, data *T) {
if err := r.ParseForm(); err != nil {
log.Panic("Parsing form: ", err)
}
@@ -36,7 +23,7 @@ func parseForm[T any](r *http.Request, data *T) {
log.Panic("Decoding form: ", err)
}
- if withCsrf, ok := any(data).(WithCsrf); ok {
+ 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 {