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 {

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

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