From 0a5ab05c88d99f82d57120324f3c0f0eb2c25de1 Mon Sep 17 00:00:00 2001 From: Arpad Ryszka Date: Sun, 31 Aug 2025 01:23:21 +0200 Subject: [PATCH] add time support --- field.go | 4 +-- field_test.go | 81 +++++++++++++++++++++++++++++++++----------------- lib.go | 8 ++--- notes.txt | 6 ---- scalar_test.go | 2 +- scan.go | 48 ++++++++++++++++++++++++++++-- scan_test.go | 50 +++++++++++++++++++++++-------- type.go | 15 ++++++++-- 8 files changed, 157 insertions(+), 57 deletions(-) diff --git a/field.go b/field.go index 4dfbf4a..4d543d3 100644 --- a/field.go +++ b/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:] } } diff --git a/field_test.go b/field_test.go index a9dd0d2..6f8a99b 100644 --- a/field_test.go +++ b/field_test.go @@ -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 { diff --git a/lib.go b/lib.go index eaa2c88..38f03db 100644 --- a/lib.go +++ b/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 diff --git a/notes.txt b/notes.txt index 8b347f2..ddac99c 100644 --- a/notes.txt +++ b/notes.txt @@ -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 diff --git a/scalar_test.go b/scalar_test.go index 50312da..9b5191c 100644 --- a/scalar_test.go +++ b/scalar_test.go @@ -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 diff --git a/scan.go b/scan.go index d6d7e2a..90250c5 100644 --- a/scan.go +++ b/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 { diff --git a/scan_test.go b/scan_test.go index 5c3e51e..9a6ecef 100644 --- a/scan_test.go +++ b/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) + } + }) } diff --git a/type.go b/type.go index bd517a4..b29e8a3 100644 --- a/type.go +++ b/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) } }