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"
"github.com/iancoleman/strcase"
"reflect"
"unicode"
"strings"
"unicode"
)
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)) {
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("no circular receiver", func(t *testing.T) {
type s struct{Foo *s; Bar int}
type s struct {
Foo *s
Bar int
}
var v s
if len(bind.BindFields(&v, bind.NamedValue("bar", 42))) != 1 {
t.Fatal()
@ -287,7 +290,7 @@ func TestField(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
var v p
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) {
type s struct{FooBar int}
type s struct{ FooBar int }
var v s
u := bind.BindFields(&v, bind.NamedValue("foo-bar", 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) {
type s struct{FooBar int}
type s struct{ FooBar int }
var v s
u := bind.BindFields(&v, bind.ValueByPath([]string{"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) {
type s struct{Foo int; Bar int}
type s struct {
Foo int
Bar int
}
var v s
u := bind.BindFields(
&v,
@ -328,7 +334,7 @@ func TestField(t *testing.T) {
})
t.Run("bind list", func(t *testing.T) {
type s struct{Foo []int}
type s struct{ Foo []int }
var v s
u := bind.BindFields(
&v,
@ -343,7 +349,7 @@ func TestField(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
u := bind.BindFields(
&v,
@ -358,7 +364,7 @@ func TestField(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
u := bind.BindFields(
&v,
@ -376,7 +382,7 @@ func TestField(t *testing.T) {
})
t.Run("list receiver", func(t *testing.T) {
var l []struct{Foo int}
var l []struct{ Foo int }
u := bind.BindFields(
&l,
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) {
type (
s0 struct{Bar int}
s1 struct{Foo []s0}
s0 struct{ Bar int }
s1 struct{ Foo []s0 }
)
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) {
type (
s0 struct{Bar int}
s1 struct{Foo []s0}
s0 struct{ Bar int }
s1 struct{ Foo []s0 }
)
v := s1{[]s0{{1}, {2}}}
@ -429,8 +435,8 @@ func TestField(t *testing.T) {
t.Run("list has invalid type", func(t *testing.T) {
type (
s0 struct{Bar chan int}
s1 struct{Foo []s0}
s0 struct{ Bar chan int }
s1 struct{ Foo []s0 }
)
v := s1{[]s0{{nil}, {nil}}}
@ -508,7 +514,7 @@ func TestField(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
u := bind.BindFields(&v, bind.NamedValue("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) {
type s struct{Foo map[string]int}
type s struct{ Foo map[string]int }
var v s
u := bind.BindFields(&v, bind.ValueByPath([]string{"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) {
type s struct{Foo map[string]int}
type s struct{ Foo map[string]int }
var v s
u := bind.BindFields(v, bind.NamedValue("foo-bar", 42))
if len(u) != 1 {
@ -551,7 +557,10 @@ func TestField(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
u := bind.BindFields(
&v,
@ -565,7 +574,10 @@ func TestField(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
u := bind.BindFields(
&v,
@ -579,7 +591,10 @@ func TestField(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
u := bind.BindFields(
&v,
@ -594,7 +609,11 @@ func TestField(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
u := bind.BindFields(
&v,
@ -609,7 +628,10 @@ func TestField(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
u := bind.BindFields(
&v,
@ -624,7 +646,10 @@ func TestField(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
u := bind.BindFields(
&v,
@ -638,7 +663,7 @@ func TestField(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
u := bind.BindFields(v, bind.NamedValue("foo", 42))
if len(u) != 1 {
@ -648,8 +673,8 @@ func TestField(t *testing.T) {
t.Run("struct with anonymous field", func(t *testing.T) {
type (
s0 struct{Foo int}
s1 struct{s0}
s0 struct{ Foo int }
s1 struct{ s0 }
)
var v s1
@ -660,7 +685,7 @@ func TestField(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
u := bind.BindFields(v, bind.NamedValue("foo-bar", 42))
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
test:
- repeated bindings
- cases of allocations
- preallocated and unallocated list sizes

44
scan.go
View File

@ -1,12 +1,36 @@
package bind
import (
"errors"
"fmt"
"reflect"
"strconv"
"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) {
bitSize := byteSize * 8
switch {
@ -38,6 +62,16 @@ func scanConvert(t reflect.Type, v any) (any, bool) {
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) {
var (
v any
@ -48,7 +82,13 @@ func scanString(t reflect.Type, s string) (any, bool) {
case reflect.Bool:
v, err = strconv.ParseBool(s)
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()))
}
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
v, err = parseUint(s, int(t.Size()))
case reflect.Float32, reflect.Float64:
@ -56,8 +96,12 @@ func scanString(t reflect.Type, s string) (any, bool) {
case reflect.String:
v = s
default:
if isTime(t) {
v, err = parseTime(s)
} else {
return nil, false
}
}
if err != nil {
return nil, false

View File

@ -3,21 +3,10 @@ package bind_test
import (
"code.squareroundforest.org/arpio/bind"
"testing"
"time"
)
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) {
var i64 int64
i := 42
@ -117,4 +106,41 @@ func TestScan(t *testing.T) {
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
import "reflect"
import (
"reflect"
"time"
)
type unpackFlag int
@ -123,6 +126,14 @@ func isInterface(t reflect.Type) bool {
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 {
switch t.Kind() {
case reflect.Bool,
@ -142,7 +153,7 @@ func isScalar(t reflect.Type) bool {
reflect.String:
return true
default:
return false
return isTime(t)
}
}