initial
This commit is contained in:
52
formscanner_test.go
Normal file
52
formscanner_test.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
28
structscanner.go
Normal file
28
structscanner.go
Normal file
@@ -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()
|
||||
}
|
||||
78
structscanner_test.go
Normal file
78
structscanner_test.go
Normal file
@@ -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")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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())
|
||||
|
||||
1
test-templates/about.html
Normal file
1
test-templates/about.html
Normal file
@@ -0,0 +1 @@
|
||||
{{define "body"}}{{template "bold" .}}{{end}}
|
||||
1
test-templates/base.html
Normal file
1
test-templates/base.html
Normal file
@@ -0,0 +1 @@
|
||||
<p>{{block "body" .}}default{{end}}</p>
|
||||
1
test-templates/contact.html
Normal file
1
test-templates/contact.html
Normal file
@@ -0,0 +1 @@
|
||||
{{define "body"}}{{join . ","}}{{end}}
|
||||
1
test-templates/home.html
Normal file
1
test-templates/home.html
Normal file
@@ -0,0 +1 @@
|
||||
{{define "body"}}HOME!{{end}}
|
||||
1
test-templates/share/bold.html
Normal file
1
test-templates/share/bold.html
Normal file
@@ -0,0 +1 @@
|
||||
{{define "bold"}}<b>{{.}}</b>{{end}}
|
||||
1
test-templates/share/italic.html
Normal file
1
test-templates/share/italic.html
Normal file
@@ -0,0 +1 @@
|
||||
{{define "italic"}}<i>{{.}}</i>{{end}}
|
||||
Reference in New Issue
Block a user