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 {

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()
@ -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,
@ -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,

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)
} }
} }