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"
|
"embed"
|
||||||
"html/template"
|
"html/template"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"log"
|
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
@@ -46,12 +45,10 @@ func loadTemplateDir(
|
|||||||
|
|
||||||
shareDir := path.Join(dirPath, "share")
|
shareDir := path.Join(dirPath, "share")
|
||||||
if _, err := fs.ReadDir(shareDir); err == nil {
|
if _, err := fs.ReadDir(shareDir); err == nil {
|
||||||
log.Printf("Parsing %s...", path.Join(shareDir, "*"))
|
|
||||||
t = template.Must(t.ParseFS(fs, path.Join(shareDir, "*")))
|
t = template.Must(t.ParseFS(fs, path.Join(shareDir, "*")))
|
||||||
}
|
}
|
||||||
|
|
||||||
if data, _ := fs.ReadFile(path.Join(dirPath, "base.html")); data != nil {
|
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)))
|
t = template.Must(t.Parse(string(data)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,7 +69,6 @@ func loadTemplateDir(
|
|||||||
}
|
}
|
||||||
|
|
||||||
filePath := path.Join(dirPath, ent.Name())
|
filePath := path.Join(dirPath, ent.Name())
|
||||||
log.Printf("Parsing %s...", filePath)
|
|
||||||
|
|
||||||
key := strings.TrimPrefix(path.Join(dirPath, ent.Name()), rootDir)
|
key := strings.TrimPrefix(path.Join(dirPath, ent.Name()), rootDir)
|
||||||
tt := template.Must(t.Clone())
|
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