add time support
This commit is contained in:
parent
b0ff605b25
commit
0a5ab05c88
4
field.go
4
field.go
@ -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:]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
8
lib.go
8
lib.go
@ -44,10 +44,10 @@ func (f Field) Name() string {
|
||||
return nameFromPath(f.path)
|
||||
}
|
||||
|
||||
func (f Field) List() bool { return f.list }
|
||||
func (f Field) Bool() bool { return f.isBool }
|
||||
func (f Field) Free() bool { return f.free }
|
||||
func (f Field) Value() any { return f.value }
|
||||
func (f Field) List() bool { return f.list }
|
||||
func (f Field) Bool() bool { return f.isBool }
|
||||
func (f Field) Free() bool { return f.free }
|
||||
func (f Field) Value() any { return f.value }
|
||||
|
||||
// it does not return fields with free keys, however, this should be obvious
|
||||
// non-struct and non-named map values return unnamed fields
|
||||
|
||||
@ -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
|
||||
|
||||
@ -219,7 +219,7 @@ func TestScalar(t *testing.T) {
|
||||
t.Fatal()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
t.Run("value has circular reference", func(t *testing.T) {
|
||||
var v any
|
||||
p := &v
|
||||
|
||||
48
scan.go
48
scan.go
@ -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:
|
||||
v, err = parseInt(s, int(t.Size()))
|
||||
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,7 +96,11 @@ func scanString(t reflect.Type, s string) (any, bool) {
|
||||
case reflect.String:
|
||||
v = s
|
||||
default:
|
||||
return nil, false
|
||||
if isTime(t) {
|
||||
v, err = parseTime(s)
|
||||
} else {
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
||||
50
scan_test.go
50
scan_test.go
@ -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
15
type.go
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user