diff --git a/formscanner_test.go b/formscanner_test.go new file mode 100644 index 0000000..34bab56 --- /dev/null +++ b/formscanner_test.go @@ -0,0 +1,52 @@ +package webutil + +import ( + "net/url" + "reflect" + "testing" +) + +func TestFormScannerTypes(t *testing.T) { + cases := []struct { + name string + form url.Values + dest any + want any + wantErr bool + }{ + {name: "string", form: url.Values{"f": {"hello"}}, dest: new(string), want: "hello"}, + {name: "int", form: url.Values{"f": {"42"}}, dest: new(int), want: int(42)}, + {name: "int8", form: url.Values{"f": {"8"}}, dest: new(int8), want: int8(8)}, + {name: "int16", form: url.Values{"f": {"16"}}, dest: new(int16), want: int16(16)}, + {name: "int32", form: url.Values{"f": {"32"}}, dest: new(int32), want: int32(32)}, + {name: "int64", form: url.Values{"f": {"64"}}, dest: new(int64), want: int64(64)}, + {name: "uint", form: url.Values{"f": {"1"}}, dest: new(uint), want: uint(1)}, + {name: "uint8", form: url.Values{"f": {"8"}}, dest: new(uint8), want: uint8(8)}, + {name: "uint16", form: url.Values{"f": {"16"}}, dest: new(uint16), want: uint16(16)}, + {name: "uint32", form: url.Values{"f": {"32"}}, dest: new(uint32), want: uint32(32)}, + {name: "uint64", form: url.Values{"f": {"64"}}, dest: new(uint64), want: uint64(64)}, + {name: "float32", form: url.Values{"f": {"1.5"}}, dest: new(float32), want: float32(1.5)}, + {name: "float64", form: url.Values{"f": {"1.5"}}, dest: new(float64), want: float64(1.5)}, + {name: "bool/present", form: url.Values{"f": {""}}, dest: new(bool), want: true}, + {name: "bool/absent", form: url.Values{}, dest: new(bool), want: false}, + {name: "int/invalid", form: url.Values{"f": {"abc"}}, dest: new(int), wantErr: true}, + {name: "unsupported type", form: url.Values{"f": {"x"}}, dest: new([]string), wantErr: true}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + s := NewFormScanner(tc.form) + s.Scan("f", tc.dest) + err := s.Error() + if tc.wantErr != (err != nil) { + t.Fatalf("error = %v, wantErr %v", err, tc.wantErr) + } + if !tc.wantErr { + got := reflect.ValueOf(tc.dest).Elem().Interface() + if got != tc.want { + t.Fatalf("got %v, want %v", got, tc.want) + } + } + }) + } +} diff --git a/structscanner.go b/structscanner.go new file mode 100644 index 0000000..fb981a6 --- /dev/null +++ b/structscanner.go @@ -0,0 +1,28 @@ +package webutil + +import ( + "errors" + "net/url" + "reflect" +) + +// ScanStruct scans form values into the exported fields of dest (must be a +// pointer to a struct) using field names as form keys. Bool fields are set to +// true when the key is present, matching HTML checkbox behaviour. +func ScanStruct(form url.Values, dest any) error { + v := reflect.ValueOf(dest) + if v.Kind() != reflect.Ptr || v.Elem().Kind() != reflect.Struct { + return errors.New("ScanStruct: dest must be a pointer to a struct") + } + v = v.Elem() + + s := NewFormScanner(form) + for key := range form { + fv := v.FieldByName(key) + if !fv.IsValid() || !fv.CanSet() { + continue + } + s.Scan(key, fv.Addr().Interface()) + } + return s.Error() +} diff --git a/structscanner_test.go b/structscanner_test.go new file mode 100644 index 0000000..d3d0bf6 --- /dev/null +++ b/structscanner_test.go @@ -0,0 +1,78 @@ +package webutil + +import ( + "net/url" + "testing" +) + +func TestScanStruct(t *testing.T) { + t.Run("fields scanned by name", func(t *testing.T) { + type S struct { + Name string + Age int + Active bool + } + var s S + err := ScanStruct(url.Values{ + "Name": {"Alice"}, + "Age": {"30"}, + "Active": {"on"}, + }, &s) + if err != nil { + t.Fatal(err) + } + if s.Name != "Alice" || s.Age != 30 || !s.Active { + t.Fatalf("unexpected: %+v", s) + } + }) + + t.Run("missing form key leaves zero value", func(t *testing.T) { + type S struct { + Name string + Age int + } + var s S + if err := ScanStruct(url.Values{"Name": {"Bob"}}, &s); err != nil { + t.Fatal(err) + } + if s.Name != "Bob" || s.Age != 0 { + t.Fatalf("unexpected: %+v", s) + } + }) + + t.Run("extra form keys ignored", func(t *testing.T) { + type S struct{ Name string } + var s S + if err := ScanStruct(url.Values{"Name": {"Alice"}, "Extra": {"ignored"}}, &s); err != nil { + t.Fatal(err) + } + }) + + t.Run("unexported fields skipped", func(t *testing.T) { + type S struct { + Name string + secret string + } + var s S + if err := ScanStruct(url.Values{"Name": {"Alice"}, "secret": {"oops"}}, &s); err != nil { + t.Fatal(err) + } + if s.secret != "" { + t.Fatal("unexported field should not be set") + } + }) + + t.Run("parse error", func(t *testing.T) { + type S struct{ Age int } + var s S + if err := ScanStruct(url.Values{"Age": {"not-a-number"}}, &s); err == nil { + t.Fatal("expected error") + } + }) + + t.Run("requires pointer to struct", func(t *testing.T) { + if err := ScanStruct(url.Values{}, "not a struct"); err == nil { + t.Fatal("expected error") + } + }) +} diff --git a/template.go b/template.go index e18f37d..fe31f9c 100644 --- a/template.go +++ b/template.go @@ -4,7 +4,6 @@ import ( "embed" "html/template" "io/fs" - "log" "path" "strings" ) @@ -46,12 +45,10 @@ func loadTemplateDir( shareDir := path.Join(dirPath, "share") if _, err := fs.ReadDir(shareDir); err == nil { - log.Printf("Parsing %s...", path.Join(shareDir, "*")) t = template.Must(t.ParseFS(fs, path.Join(shareDir, "*"))) } if data, _ := fs.ReadFile(path.Join(dirPath, "base.html")); data != nil { - log.Printf("Parsing %s...", path.Join(dirPath, "base.html")) t = template.Must(t.Parse(string(data))) } @@ -72,7 +69,6 @@ func loadTemplateDir( } filePath := path.Join(dirPath, ent.Name()) - log.Printf("Parsing %s...", filePath) key := strings.TrimPrefix(path.Join(dirPath, ent.Name()), rootDir) tt := template.Must(t.Clone()) diff --git a/test-templates/about.html b/test-templates/about.html new file mode 100644 index 0000000..dfaaaa0 --- /dev/null +++ b/test-templates/about.html @@ -0,0 +1 @@ +{{define "body"}}{{template "bold" .}}{{end}} diff --git a/test-templates/base.html b/test-templates/base.html new file mode 100644 index 0000000..f1c87e6 --- /dev/null +++ b/test-templates/base.html @@ -0,0 +1 @@ +

{{block "body" .}}default{{end}}

diff --git a/test-templates/contact.html b/test-templates/contact.html new file mode 100644 index 0000000..a4394c9 --- /dev/null +++ b/test-templates/contact.html @@ -0,0 +1 @@ +{{define "body"}}{{join . ","}}{{end}} diff --git a/test-templates/home.html b/test-templates/home.html new file mode 100644 index 0000000..fc06425 --- /dev/null +++ b/test-templates/home.html @@ -0,0 +1 @@ +{{define "body"}}HOME!{{end}} diff --git a/test-templates/share/bold.html b/test-templates/share/bold.html new file mode 100644 index 0000000..090a255 --- /dev/null +++ b/test-templates/share/bold.html @@ -0,0 +1 @@ +{{define "bold"}}{{.}}{{end}} diff --git a/test-templates/share/italic.html b/test-templates/share/italic.html new file mode 100644 index 0000000..e9502e5 --- /dev/null +++ b/test-templates/share/italic.html @@ -0,0 +1 @@ +{{define "italic"}}{{.}}{{end}}