1
0

add time support

This commit is contained in:
Arpad Ryszka 2025-08-31 01:23:21 +02:00
parent b0ff605b25
commit 0a5ab05c88
8 changed files with 157 additions and 57 deletions

View File

@ -4,8 +4,8 @@ import (
"fmt" "fmt"
"github.com/iancoleman/strcase" "github.com/iancoleman/strcase"
"reflect" "reflect"
"unicode"
"strings" "strings"
"unicode"
) )
func pathString(f Field) string { func pathString(f Field) string {
@ -298,7 +298,7 @@ func trimNameAndPath(name string, values []Field) []Field {
} }
if strings.HasPrefix(v[i].name, fmt.Sprintf("%s-", name)) { if strings.HasPrefix(v[i].name, fmt.Sprintf("%s-", name)) {
v[i].name = v[i].name[len(name) + 1:] v[i].name = v[i].name[len(name)+1:]
} }
} }

View File

@ -279,7 +279,10 @@ func TestField(t *testing.T) {
t.Run("bind fields", func(t *testing.T) { t.Run("bind fields", func(t *testing.T) {
t.Run("no circular receiver", func(t *testing.T) { t.Run("no circular receiver", func(t *testing.T) {
type s struct{Foo *s; Bar int} type s struct {
Foo *s
Bar int
}
var v s var v s
if len(bind.BindFields(&v, bind.NamedValue("bar", 42))) != 1 { if len(bind.BindFields(&v, bind.NamedValue("bar", 42))) != 1 {
t.Fatal() t.Fatal()
@ -287,7 +290,7 @@ func TestField(t *testing.T) {
}) })
t.Run("no circular valeu", func(t *testing.T) { t.Run("no circular valeu", func(t *testing.T) {
type s struct{Foo int} type s struct{ Foo int }
type p *p type p *p
var v p var v p
if len(bind.BindFields(&v, bind.NamedValue("foo", 42))) != 1 { if len(bind.BindFields(&v, bind.NamedValue("foo", 42))) != 1 {
@ -296,7 +299,7 @@ func TestField(t *testing.T) {
}) })
t.Run("set by name", func(t *testing.T) { t.Run("set by name", func(t *testing.T) {
type s struct{FooBar int} type s struct{ FooBar int }
var v s var v s
u := bind.BindFields(&v, bind.NamedValue("foo-bar", 42)) u := bind.BindFields(&v, bind.NamedValue("foo-bar", 42))
if len(u) != 0 || v.FooBar != 42 { if len(u) != 0 || v.FooBar != 42 {
@ -305,7 +308,7 @@ func TestField(t *testing.T) {
}) })
t.Run("set by path", func(t *testing.T) { t.Run("set by path", func(t *testing.T) {
type s struct{FooBar int} type s struct{ FooBar int }
var v s var v s
u := bind.BindFields(&v, bind.ValueByPath([]string{"FooBar"}, 42)) u := bind.BindFields(&v, bind.ValueByPath([]string{"FooBar"}, 42))
if len(u) != 0 || v.FooBar != 42 { if len(u) != 0 || v.FooBar != 42 {
@ -314,7 +317,10 @@ func TestField(t *testing.T) {
}) })
t.Run("fail to bind", func(t *testing.T) { t.Run("fail to bind", func(t *testing.T) {
type s struct{Foo int; Bar int} type s struct {
Foo int
Bar int
}
var v s var v s
u := bind.BindFields( u := bind.BindFields(
&v, &v,
@ -328,7 +334,7 @@ func TestField(t *testing.T) {
}) })
t.Run("bind list", func(t *testing.T) { t.Run("bind list", func(t *testing.T) {
type s struct{Foo []int} type s struct{ Foo []int }
var v s var v s
u := bind.BindFields( u := bind.BindFields(
&v, &v,
@ -343,7 +349,7 @@ func TestField(t *testing.T) {
}) })
t.Run("bind list of structs", func(t *testing.T) { t.Run("bind list of structs", func(t *testing.T) {
type s struct{Foo []struct{Bar int}} type s struct{ Foo []struct{ Bar int } }
var v s var v s
u := bind.BindFields( u := bind.BindFields(
&v, &v,
@ -358,7 +364,7 @@ func TestField(t *testing.T) {
}) })
t.Run("bind list in list", func(t *testing.T) { t.Run("bind list in list", func(t *testing.T) {
type s struct{Foo []struct{Bar []int}} type s struct{ Foo []struct{ Bar []int } }
var v s var v s
u := bind.BindFields( u := bind.BindFields(
&v, &v,
@ -376,7 +382,7 @@ func TestField(t *testing.T) {
}) })
t.Run("list receiver", func(t *testing.T) { t.Run("list receiver", func(t *testing.T) {
var l []struct{Foo int} var l []struct{ Foo int }
u := bind.BindFields( u := bind.BindFields(
&l, &l,
bind.NamedValue("foo", 21), bind.NamedValue("foo", 21),
@ -391,8 +397,8 @@ func TestField(t *testing.T) {
t.Run("list short and cannot be set", func(t *testing.T) { t.Run("list short and cannot be set", func(t *testing.T) {
type ( type (
s0 struct{Bar int} s0 struct{ Bar int }
s1 struct{Foo []s0} s1 struct{ Foo []s0 }
) )
v := s1{[]s0{{1}, {2}}} v := s1{[]s0{{1}, {2}}}
@ -410,8 +416,8 @@ func TestField(t *testing.T) {
t.Run("list short and gets reset", func(t *testing.T) { t.Run("list short and gets reset", func(t *testing.T) {
type ( type (
s0 struct{Bar int} s0 struct{ Bar int }
s1 struct{Foo []s0} s1 struct{ Foo []s0 }
) )
v := s1{[]s0{{1}, {2}}} v := s1{[]s0{{1}, {2}}}
@ -429,8 +435,8 @@ func TestField(t *testing.T) {
t.Run("list has invalid type", func(t *testing.T) { t.Run("list has invalid type", func(t *testing.T) {
type ( type (
s0 struct{Bar chan int} s0 struct{ Bar chan int }
s1 struct{Foo []s0} s1 struct{ Foo []s0 }
) )
v := s1{[]s0{{nil}, {nil}}} v := s1{[]s0{{nil}, {nil}}}
@ -508,7 +514,7 @@ func TestField(t *testing.T) {
}) })
t.Run("allocate scalar map", func(t *testing.T) { t.Run("allocate scalar map", func(t *testing.T) {
type s struct{Foo map[string]int} type s struct{ Foo map[string]int }
var v s var v s
u := bind.BindFields(&v, bind.NamedValue("foo-bar", 42)) u := bind.BindFields(&v, bind.NamedValue("foo-bar", 42))
if len(u) != 0 || len(v.Foo) != 1 || v.Foo["bar"] != 42 { if len(u) != 0 || len(v.Foo) != 1 || v.Foo["bar"] != 42 {
@ -517,7 +523,7 @@ func TestField(t *testing.T) {
}) })
t.Run("scalar map addressing via path", func(t *testing.T) { t.Run("scalar map addressing via path", func(t *testing.T) {
type s struct{Foo map[string]int} type s struct{ Foo map[string]int }
var v s var v s
u := bind.BindFields(&v, bind.ValueByPath([]string{"Foo", "Bar"}, 42)) u := bind.BindFields(&v, bind.ValueByPath([]string{"Foo", "Bar"}, 42))
if len(u) != 0 || len(v.Foo) != 1 || v.Foo["Bar"] != 42 { if len(u) != 0 || len(v.Foo) != 1 || v.Foo["Bar"] != 42 {
@ -534,7 +540,7 @@ func TestField(t *testing.T) {
}) })
t.Run("scalar map cannot be set", func(t *testing.T) { t.Run("scalar map cannot be set", func(t *testing.T) {
type s struct{Foo map[string]int} type s struct{ Foo map[string]int }
var v s var v s
u := bind.BindFields(v, bind.NamedValue("foo-bar", 42)) u := bind.BindFields(v, bind.NamedValue("foo-bar", 42))
if len(u) != 1 { if len(u) != 1 {
@ -551,7 +557,10 @@ func TestField(t *testing.T) {
}) })
t.Run("struct fields", func(t *testing.T) { t.Run("struct fields", func(t *testing.T) {
type s struct{Foo int; Bar struct { Baz string }} type s struct {
Foo int
Bar struct{ Baz string }
}
var v s var v s
u := bind.BindFields( u := bind.BindFields(
&v, &v,
@ -565,7 +574,10 @@ func TestField(t *testing.T) {
}) })
t.Run("non-existing field", func(t *testing.T) { t.Run("non-existing field", func(t *testing.T) {
type s struct{Foo int; Bar struct { Baz string }} type s struct {
Foo int
Bar struct{ Baz string }
}
var v s var v s
u := bind.BindFields( u := bind.BindFields(
&v, &v,
@ -579,7 +591,10 @@ func TestField(t *testing.T) {
}) })
t.Run("too many fields", func(t *testing.T) { t.Run("too many fields", func(t *testing.T) {
type s struct{Foo int; Bar struct { Baz string }} type s struct {
Foo int
Bar struct{ Baz string }
}
var v s var v s
u := bind.BindFields( u := bind.BindFields(
&v, &v,
@ -594,7 +609,11 @@ func TestField(t *testing.T) {
}) })
t.Run("pointer fields", func(t *testing.T) { t.Run("pointer fields", func(t *testing.T) {
type s struct{Foo *int; Bar *struct { Baz *string }; Qux *[]struct{Quux string}} type s struct {
Foo *int
Bar *struct{ Baz *string }
Qux *[]struct{ Quux string }
}
var v s var v s
u := bind.BindFields( u := bind.BindFields(
&v, &v,
@ -609,7 +628,10 @@ func TestField(t *testing.T) {
}) })
t.Run("unsupported pointer fields", func(t *testing.T) { t.Run("unsupported pointer fields", func(t *testing.T) {
type s struct{Foo *int; Bar *chan int} type s struct {
Foo *int
Bar *chan int
}
var v s var v s
u := bind.BindFields( u := bind.BindFields(
&v, &v,
@ -624,7 +646,10 @@ func TestField(t *testing.T) {
}) })
t.Run("struct fields by path", func(t *testing.T) { t.Run("struct fields by path", func(t *testing.T) {
type s struct{Foo int; Bar struct { Baz string }} type s struct {
Foo int
Bar struct{ Baz string }
}
var v s var v s
u := bind.BindFields( u := bind.BindFields(
&v, &v,
@ -638,7 +663,7 @@ func TestField(t *testing.T) {
}) })
t.Run("cannot set field", func(t *testing.T) { t.Run("cannot set field", func(t *testing.T) {
type s struct{Foo int} type s struct{ Foo int }
var v s var v s
u := bind.BindFields(v, bind.NamedValue("foo", 42)) u := bind.BindFields(v, bind.NamedValue("foo", 42))
if len(u) != 1 { if len(u) != 1 {
@ -648,8 +673,8 @@ func TestField(t *testing.T) {
t.Run("struct with anonymous field", func(t *testing.T) { t.Run("struct with anonymous field", func(t *testing.T) {
type ( type (
s0 struct{Foo int} s0 struct{ Foo int }
s1 struct{s0} s1 struct{ s0 }
) )
var v s1 var v s1
@ -660,7 +685,7 @@ func TestField(t *testing.T) {
}) })
t.Run("receiver cannot be set", func(t *testing.T) { t.Run("receiver cannot be set", func(t *testing.T) {
type s struct{Foo *struct{Bar int}} type s struct{ Foo *struct{ Bar int } }
var v s var v s
u := bind.BindFields(v, bind.NamedValue("foo-bar", 42)) u := bind.BindFields(v, bind.NamedValue("foo-bar", 42))
if len(u) != 1 { if len(u) != 1 {

View File

@ -1,7 +1 @@
add time and duration support
don't change the input value when bind returns false
track down the cases when reflect can panic track down the cases when reflect can panic
test:
- repeated bindings
- cases of allocations
- preallocated and unallocated list sizes

44
scan.go
View File

@ -1,12 +1,36 @@
package bind package bind
import ( import (
"errors"
"fmt" "fmt"
"reflect" "reflect"
"strconv" "strconv"
"strings" "strings"
"time"
) )
var timeLayouts = []string{
time.RFC3339,
time.RFC3339Nano,
time.DateTime,
time.DateOnly,
time.TimeOnly,
time.RFC822,
time.RFC822Z,
time.RFC850,
time.RFC1123,
time.RFC1123Z,
time.ANSIC,
time.UnixDate,
time.RubyDate,
time.Kitchen,
time.Layout,
time.Stamp,
time.StampMilli,
time.StampMicro,
time.StampNano,
}
func intParse[T any](parse func(string, int, int) (T, error), s string, byteSize int) (T, error) { func intParse[T any](parse func(string, int, int) (T, error), s string, byteSize int) (T, error) {
bitSize := byteSize * 8 bitSize := byteSize * 8
switch { switch {
@ -38,6 +62,16 @@ func scanConvert(t reflect.Type, v any) (any, bool) {
return r.Convert(t).Interface(), true return r.Convert(t).Interface(), true
} }
func parseTime(s string) (any, error) {
for _, l := range timeLayouts {
if t, err := time.Parse(l, s); err == nil {
return t, nil
}
}
return time.Time{}, errors.New("failed to parse time")
}
func scanString(t reflect.Type, s string) (any, bool) { func scanString(t reflect.Type, s string) (any, bool) {
var ( var (
v any v any
@ -48,7 +82,13 @@ func scanString(t reflect.Type, s string) (any, bool) {
case reflect.Bool: case reflect.Bool:
v, err = strconv.ParseBool(s) v, err = strconv.ParseBool(s)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
if isDuration(t) {
v, err = time.ParseDuration(s)
}
if err != nil {
v, err = parseInt(s, int(t.Size())) v, err = parseInt(s, int(t.Size()))
}
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
v, err = parseUint(s, int(t.Size())) v, err = parseUint(s, int(t.Size()))
case reflect.Float32, reflect.Float64: case reflect.Float32, reflect.Float64:
@ -56,8 +96,12 @@ func scanString(t reflect.Type, s string) (any, bool) {
case reflect.String: case reflect.String:
v = s v = s
default: default:
if isTime(t) {
v, err = parseTime(s)
} else {
return nil, false return nil, false
} }
}
if err != nil { if err != nil {
return nil, false return nil, false

View File

@ -3,21 +3,10 @@ package bind_test
import ( import (
"code.squareroundforest.org/arpio/bind" "code.squareroundforest.org/arpio/bind"
"testing" "testing"
"time"
) )
func TestScan(t *testing.T) { func TestScan(t *testing.T) {
// conversion
// unscannable
// bool
// int
// int sized
// uint
// uint sized
// binary
// octal
// hexa
// num parse failing
t.Run("conversion", func(t *testing.T) { t.Run("conversion", func(t *testing.T) {
var i64 int64 var i64 int64
i := 42 i := 42
@ -117,4 +106,41 @@ func TestScan(t *testing.T) {
t.Fatal() t.Fatal()
} }
}) })
t.Run("duration", func(t *testing.T) {
var d time.Duration
if !bind.BindScalar(&d, "9s") || d != 9*time.Second {
t.Fatal(d)
}
})
t.Run("duration convert", func(t *testing.T) {
type dur time.Duration
var d dur
if !bind.BindScalar(&d, "9s") || d != dur(9*time.Second) {
t.Fatal(d)
}
})
t.Run("time rfc", func(t *testing.T) {
var tim time.Time
if !bind.BindScalar(&tim, "2025-08-31T01:14:55+02:00") || tim.Year() != 2025 {
t.Fatal(tim)
}
})
t.Run("time only", func(t *testing.T) {
var tim time.Time
if !bind.BindScalar(&tim, "01:14:55") || tim.Second() != 55 {
t.Fatal(tim)
}
})
t.Run("time convert", func(t *testing.T) {
type timey time.Time
var tim timey
if !bind.BindScalar(&tim, "2025-08-31T01:14:55+02:00") || time.Time(tim).Year() != 2025 {
t.Fatal(tim)
}
})
} }

15
type.go
View File

@ -1,6 +1,9 @@
package bind package bind
import "reflect" import (
"reflect"
"time"
)
type unpackFlag int type unpackFlag int
@ -123,6 +126,14 @@ func isInterface(t reflect.Type) bool {
return t.Kind() == reflect.Interface && t.NumMethod() > 0 return t.Kind() == reflect.Interface && t.NumMethod() > 0
} }
func isTime(t reflect.Type) bool {
return t.ConvertibleTo(reflect.TypeFor[time.Time]())
}
func isDuration(t reflect.Type) bool {
return t.ConvertibleTo(reflect.TypeFor[time.Duration]())
}
func isScalar(t reflect.Type) bool { func isScalar(t reflect.Type) bool {
switch t.Kind() { switch t.Kind() {
case reflect.Bool, case reflect.Bool,
@ -142,7 +153,7 @@ func isScalar(t reflect.Type) bool {
reflect.String: reflect.String:
return true return true
default: default:
return false return isTime(t)
} }
} }