1
0

refactor field list and field value list

This commit is contained in:
Arpad Ryszka 2025-09-03 22:12:13 +02:00
parent bcadfd8b71
commit a7b031f3b4
9 changed files with 330 additions and 221 deletions

205
field.go
View File

@ -51,76 +51,12 @@ func fieldFromType(name string, t reflect.Type) Field {
var f Field
ft := unpackType(t, pointer|slice)
f.typ = scalarType(ft)
f.size = scalarSize(ft)
f.list = acceptsList(t)
f.name = strcase.ToKebab(name)
f.path = []string{name}
return f
}
func nilField() Field {
var fi Field
fi.typ = Any
return fi
}
func fieldFromValue(v reflect.Value) Field {
var fi Field
fi.value = v.Interface()
fi.typ = scalarType(v.Type())
fi.size = valueSize(v)
return fi
}
func fields(t reflect.Type) []Field {
t = unpackType(t, pointer)
list := t.Kind() == reflect.Slice
t = unpackType(t, pointer|slice)
if !isStruct(t) {
return nil
}
var f []Field
for i := 0; i < t.NumField(); i++ {
tfi := t.Field(i)
if !exported(tfi.Name) && !tfi.Anonymous {
continue
}
if acceptsScalar(tfi.Type) {
fi := fieldFromType(tfi.Name, tfi.Type)
f = append(f, fi)
continue
}
ffi := fields(tfi.Type)
if !tfi.Anonymous {
for i := range ffi {
ffi[i].name = fmt.Sprintf(
"%s-%s",
strcase.ToKebab(tfi.Name),
ffi[i].name,
)
ffi[i].path = append(
[]string{tfi.Name},
ffi[i].path...,
)
}
}
f = append(f, ffi...)
}
if list {
for i := range f {
f[i].list = true
}
}
return f
}
func prependFieldName(name string, f []Field) {
for i := range f {
if f[i].name == "" {
@ -134,19 +70,74 @@ func prependFieldName(name string, f []Field) {
}
}
func fields(t reflect.Type) []Field {
t = unpackType(t, pointer|slice)
if !isStruct(t) {
return nil
}
var f []Field
for i := 0; i < t.NumField(); i++ {
fi := t.Field(i)
if !exported(fi.Name) && !fi.Anonymous {
continue
}
if acceptsScalar(fi.Type) {
fi := fieldFromType(fi.Name, fi.Type)
f = append(f, fi)
continue
}
ffi := fields(fi.Type)
if !fi.Anonymous {
prependFieldName(fi.Name, ffi)
}
if acceptsList(fi.Type) {
for i := range ffi {
ffi[i].list = true
}
}
f = append(f, ffi...)
}
return f
}
func freeFields(f []Field) {
for i := range f {
f[i].free = true
}
}
func scalarFieldValues(name string, t reflect.Type, v reflect.Value) []Field {
var f []Field
vv := unpackAllValues(v)
for _, vvi := range vv {
if !vvi.IsValid() || !isScalar(vvi.Type()) {
continue
}
fi := fieldFromType(name, t)
fi.value = vvi.Interface()
f = append(f, fi)
}
if len(f) == 0 {
f = append(f, fieldFromType(name, t))
}
return f
}
func scalarMapFields(v reflect.Value) []Field {
var f []Field
for _, key := range v.MapKeys() {
name := key.Interface().(string)
value := v.MapIndex(key)
fk := fieldValues(value)
prependFieldName(name, fk)
fk := scalarFieldValues(name, v.Type().Elem(), value)
freeFields(fk)
f = append(f, fk...)
}
@ -154,55 +145,60 @@ func scalarMapFields(v reflect.Value) []Field {
return f
}
func fieldValues(v reflect.Value) []Field {
if !v.IsValid() {
return []Field{nilField()}
}
func structFields(v reflect.Value) []Field {
var f []Field
v = unpackValue(v, pointer|anytype|iface)
t := v.Type()
if isScalar(t) {
return []Field{fieldFromValue(v)}
for i := 0; i < t.NumField(); i++ {
fi := t.Field(i)
if !exported(fi.Name) && !fi.Anonymous {
continue
}
if v.Kind() == reflect.Slice {
for i := 0; i < v.Len(); i++ {
f = append(f, fieldValues(v.Index(i))...)
vi := v.Field(i)
if acceptsScalar(fi.Type) {
f = append(f, scalarFieldValues(fi.Name, fi.Type, vi)...)
continue
}
ffi := fieldValues(vi)
if !fi.Anonymous {
prependFieldName(fi.Name, ffi)
}
if acceptsList(fi.Type) {
for i := range ffi {
ffi[i].list = true
}
}
f = append(f, ffi...)
}
return f
}
}
if isScalarMap(t) {
return scalarMapFields(v)
}
if !isStruct(t) {
func fieldValues(v reflect.Value) []Field {
if !v.IsValid() {
return nil
}
for i := 0; i < t.NumField(); i++ {
tfi := t.Field(i)
if !exported(tfi.Name) && !tfi.Anonymous {
var values []reflect.Value
allValues := unpackAllValues(v)
for _, vi := range allValues {
if acceptsFields(vi.Type()) {
values = append(values, vi)
}
}
var f []Field
for _, vi := range values {
ti := vi.Type()
if isScalarMap(ti) {
f = append(f, scalarMapFields(vi)...)
continue
}
vfi := v.Field(i)
if tfi.Anonymous && !vfi.CanAddr() {
continue
}
vfi = unpackValue(vfi, pointer|iface|anytype)
switch {
case tfi.Anonymous:
ff := fieldValues(vfi)
f = append(f, ff...)
default:
ff := fieldValues(vfi)
prependFieldName(tfi.Name, ff)
f = append(f, ff...)
}
f = append(f, structFields(vi)...)
}
return f
@ -218,11 +214,11 @@ func takeFieldValues(f []Field) []any {
}
func bindScalarField(receiver reflect.Value, values []Field) bool {
v := takeFieldValues(values)
if !receiver.CanSet() {
return bindScalar(receiver, v)
return false
}
v := takeFieldValues(values)
rv, ok := allocate(receiver.Type(), len(v))
if !ok {
return false
@ -379,7 +375,8 @@ func bindField(receiver reflect.Value, values []Field) bool {
}
if values[0].name == "" && len(values[0].path) == 0 {
return bindScalarField(receiver, values)
ret := bindScalarField(receiver, values)
return ret
}
listReceiver := unpackValue(receiver, pointer|iface|anytype)

View File

@ -51,6 +51,7 @@ func TestField(t *testing.T) {
Hola []int
}
t.Run("mixed", func(t *testing.T) {
f := bind.Fields[s2]()
if len(f) != 6 {
t.Fatal(notation.Sprintwt(f))
@ -90,6 +91,7 @@ func TestField(t *testing.T) {
if !slices.Equal(hola.Path(), []string{"Hola"}) || !hola.List() {
t.Fatal(hola.Name())
}
})
t.Run("cannot have fields", func(t *testing.T) {
type i []int
@ -109,19 +111,33 @@ func TestField(t *testing.T) {
t.Run("list", func(t *testing.T) {
type s struct{ Foo int }
f := bind.Fields[[]s]()
if len(f) != 1 || !f[0].List() {
if len(f) != 1 || f[0].List() {
t.Fatal()
}
})
t.Run("anonymous fields", func(t *testing.T) {
type s0 struct{ Foo int }
type s1 struct {
s0
Bar int
}
f := bind.Fields[s1]()
if len(f) != 2 || f[0].Name() != "foo" || f[1].Name() != "bar" {
t.Fatal(notation.Sprint(f))
}
})
})
t.Run("field values", func(t *testing.T) {
t.Run("has circular reference", func(t *testing.T) {
type s struct{ Foo any }
var v s
v.Foo = v
if len(bind.FieldValues(v)) != 0 {
t.Fatal()
v.Foo = &v
f := bind.FieldValues(v)
if len(f) != 0 {
t.Fatal(notation.Sprint(f))
}
})
@ -158,7 +174,7 @@ func TestField(t *testing.T) {
t.Run("not a struct", func(t *testing.T) {
v := []int{21, 42, 84}
f := bind.FieldValues(v)
if len(f) != 3 || f[0].Value() != 21 || f[1].Value() != 42 || f[2].Value() != 84 {
if len(f) != 0 {
t.Fatal()
}
})
@ -217,7 +233,7 @@ func TestField(t *testing.T) {
type s struct{ Foo any }
v := s{Foo: []any{map[string]int{"foo": 42}}}
f := bind.FieldValues(v)
if len(f) != 1 || f[0].Name() != "foo-foo" || f[0].Value() != 42 {
if len(f) != 1 || f[0].Name() != "foo" || f[0].Value() != nil {
t.Fatal(notation.Sprintwt(f))
}
})
@ -226,9 +242,8 @@ func TestField(t *testing.T) {
type s struct{ Foo any }
v := s{Foo: []any{s{Foo: 21}, s{Foo: 42}}}
f := bind.FieldValues(v)
if len(f) != 2 ||
f[0].Name() != "foo-foo" || f[0].Value() != 21 ||
f[1].Name() != "foo-foo" || f[1].Value() != 42 {
if len(f) != 1 ||
f[0].Name() != "foo" || f[0].Value() != nil {
t.Fatal()
}
})
@ -248,12 +263,13 @@ func TestField(t *testing.T) {
s0
Bar int
}
var v s1
v.Foo = 21
v.Bar = 42
f := bind.FieldValues(v)
if len(f) != 1 || f[0].Name() != "bar" || f[0].Value() != 42 {
t.Fatal()
if len(f) != 2 || f[0].Name() != "foo" || f[0].Value() != 21 || f[1].Name() != "bar" || f[1].Value() != 42 {
t.Fatal(notation.Sprint(f))
}
})
@ -273,6 +289,75 @@ func TestField(t *testing.T) {
t.Fatal(notation.Sprintwt(f))
}
})
t.Run("any type field nil", func(t *testing.T) {
type s struct{ Foo any }
var v s
f := bind.FieldValues(v)
if len(f) != 1 || f[0].Name() != "foo" || f[0].Type() != bind.Any {
t.Fatal()
}
})
t.Run("any type field not nil", func(t *testing.T) {
type s struct{ Foo any }
v := s{42}
f := bind.FieldValues(v)
if len(f) != 1 || f[0].Name() != "foo" || f[0].Type() != bind.Any || f[0].Value() != 42 {
t.Fatal(notation.Sprintt(v))
}
})
t.Run("slice field empty", func(t *testing.T) {
type s struct{ Foo []int }
var v s
f := bind.FieldValues(v)
if len(f) != 1 || f[0].Name() != "foo" || !f[0].List() || f[0].Type() != bind.Int || f[0].Value() != nil {
t.Fatal(notation.Sprint(f))
}
})
t.Run("slice field not empty", func(t *testing.T) {
type s struct{ Foo []int }
v := s{[]int{21, 42, 84}}
f := bind.FieldValues(v)
if len(f) != 3 ||
f[0].Name() != "foo" || !f[0].List() || f[0].Type() != bind.Int || f[0].Value() != 21 ||
f[1].Name() != "foo" || !f[1].List() || f[1].Type() != bind.Int || f[1].Value() != 42 ||
f[2].Name() != "foo" || !f[2].List() || f[2].Type() != bind.Int || f[2].Value() != 84 {
t.Fatal()
}
})
t.Run("slice field with any type empty", func(t *testing.T) {
type s struct{ Foo []any }
var v s
f := bind.FieldValues(v)
if len(f) != 1 || f[0].Name() != "foo" || !f[0].List() || f[0].Type() != bind.Any || f[0].Value() != nil {
t.Fatal()
}
})
t.Run("slice field with any type not empty", func(t *testing.T) {
type s struct{ Foo []any }
v := s{[]any{21, 42, 84}}
f := bind.FieldValues(v)
if len(f) != 3 ||
f[0].Name() != "foo" || !f[0].List() || f[0].Type() != bind.Any || f[0].Value() != 21 ||
f[1].Name() != "foo" || !f[1].List() || f[1].Type() != bind.Any || f[1].Value() != 42 ||
f[2].Name() != "foo" || !f[2].List() || f[2].Type() != bind.Any || f[2].Value() != 84 {
t.Fatal(notation.Sprintw(f))
}
})
t.Run("struct in its own field via interface", func(t *testing.T) {
var s struct{ F any }
s.F = s
f := bind.FieldValues(s)
if len(f) != 1 || f[0].Name() != "f" || f[0].Value() != nil {
t.Fatal(notation.Sprint(f))
}
})
})
t.Run("bind fields", func(t *testing.T) {
@ -375,7 +460,7 @@ func TestField(t *testing.T) {
len(v.Foo[0].Bar) != 1 || v.Foo[0].Bar[0] != 21 ||
len(v.Foo[1].Bar) != 1 || v.Foo[1].Bar[0] != 42 ||
len(v.Foo[2].Bar) != 1 || v.Foo[2].Bar[0] != 84 {
t.Fatal()
t.Fatal(notation.Sprintw(v))
}
})

81
lib.go
View File

@ -1,18 +1,30 @@
// Package bind provides functions to work with flattened lists of structure fields.
package bind
// Scalar defines scalar types that can be used as field values and are not traversed.
type Scalar int
import "reflect"
// Field type represents the possible types of a field.
type FieldType = reflect.Kind
const (
Any Scalar = iota
Bool
Int
Uint
Float
String
Duration
Time
Bool FieldType = reflect.Bool
Int FieldType = reflect.Int
Int8 FieldType = reflect.Int8
Int16 FieldType = reflect.Int16
Int32 FieldType = reflect.Int32
Int64 FieldType = reflect.Int64
Uint FieldType = reflect.Uint
Uint8 FieldType = reflect.Uint8
Uint16 FieldType = reflect.Uint16
Uint32 FieldType = reflect.Uint32
Uint64 FieldType = reflect.Uint64
Uintptr FieldType = reflect.Uintptr
Float32 FieldType = reflect.Float32
Float64 FieldType = reflect.Float64
Any FieldType = reflect.Interface
Struct FieldType = reflect.Struct
Duration FieldType = 0xfffe
Time FieldType = 0xffff
)
// Field is a field of structure or a compatible map. It is used as the output of the inspection functions, and
@ -22,8 +34,7 @@ type Field struct {
path []string
name string
list bool
typ Scalar
size int
typ reflect.Kind
free bool
value any
}
@ -41,6 +52,11 @@ func BindScalarCreate[T any](value ...any) (T, bool) {
return bindScalarCreateReflect[T](value)
}
// BindScalarCreate is like BindScalar, but it allocates the receiver from the type t.
func BindScalarCreateWith(t reflect.Type, value ...any) (reflect.Value, bool) {
return reflect.Value{}, false
}
// ValueByPath defines a field for input to BindFields or BindFieldsCreate. It defines the field by its exact
// field path in the receiver structure.
func ValueByPath(path []string, value any) Field {
@ -77,7 +93,7 @@ func (f Field) Name() string {
func (f Field) List() bool { return f.list }
// Type returns the scalar type of the field.
func (f Field) Type() Scalar {
func (f Field) Type() reflect.Kind {
if !f.input {
return f.typ
}
@ -85,15 +101,6 @@ func (f Field) Type() Scalar {
return scalarTypeReflect(f.value)
}
// Size returns the bitsize of the field.
func (f Field) Size() int {
if !f.input {
return f.size
}
return valueSizeReflect(f.value)
}
// Free indicates that the field was found in a map.
func (f Field) Free() bool {
return f.free
@ -109,6 +116,11 @@ func Fields[T any]() []Field {
return fieldsReflect[T]()
}
// Fields of is like Fields but uses type t as the input.
func FieldsOf(t reflect.Type) []Field {
return nil
}
// FieldValues returns the fields of a structure value recursively. It traverses through pointers, slices and
// interfaces.
func FieldValues(structure any) []Field {
@ -127,22 +139,47 @@ func BindFieldsCreate[T any](values ...Field) (T, []Field) {
return bindFieldsCreateReflect[T](values)
}
// BindFieldsCreate is like BindFields, but it allocates the receiver from type t.
func BindFieldsCreateWith(t reflect.Type, values ...Field) (reflect.Value, []Field) {
return reflect.Value{}, nil
}
// AcceptsScalar checks if a type can be used with BindScalarCreate or the values of the type with BindScalar.
func AcceptsScalar[T any]() bool {
return acceptsScalarReflect[T]()
}
// TypeAcceptsScalar is like AcceptsScalar, but uses type t as input.
func TypeAcceptsScalar(t reflect.Type) bool {
return false
}
// AcceptsFields checks if a type can be used with BindFieldsCreate or the values of the type with BindFields.
func AcceptsFields[T any]() bool {
return acceptsFieldsReflect[T]()
}
// TypeAcceptsFields is like AcceptsFields, but uses type t as input.
func TypeAcceptsFields(t reflect.Type) bool {
return false
}
// AcceptsList checks if a type can be used to bind multiple values.
func AcceptsList[T any]() bool {
return acceptsListReflect[T]()
}
// TypeAcceptsList is like AcceptsList, but uses type t as input.
func TypeAcceptsList(t reflect.Type) bool {
return false
}
// Bindable is the same as AcceptsScalar[T]() || AcceptsFields[T]().
func Bindable[T any]() bool {
return bindableReflect[T]()
}
// BindableType is like Bindable, but uses type t as input.
func BindableType(t reflect.Type) bool {
return false
}

View File

@ -17,23 +17,23 @@ func bindScalar(receiver reflect.Value, values []any) bool {
return false
}
r := reflect.ValueOf(values[0])
r = unpackValue(r, pointer|iface|slice)
if !r.IsValid() && !isAny(receiver.Type()) {
v := reflect.ValueOf(values[0])
v = unpackValue(v, pointer|iface)
if !v.IsValid() && !isAny(receiver.Type()) {
return false
}
if !r.IsValid() {
if !v.IsValid() {
receiver.Set(reflect.Zero(receiver.Type()))
return true
}
v, ok := scan(receiver.Type(), r.Interface())
vv, ok := scan(receiver.Type(), v.Interface())
if !ok {
return false
}
receiver.Set(reflect.ValueOf(v))
receiver.Set(reflect.ValueOf(vv))
return true
}

View File

@ -44,7 +44,7 @@ func TestScalar(t *testing.T) {
t.Run("cannot scan", func(t *testing.T) {
var i int
if bind.BindScalar(i, "foo") {
if bind.BindScalar(&i, "foo") {
t.Fatal()
}
})

View File

@ -90,7 +90,7 @@ func scanString(t reflect.Type, s string) (any, bool) {
v, err = time.ParseDuration(s)
}
if err != nil {
if !isDuration(t) || err != nil {
v, err = parseInt(s, int(t.Size()))
}
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:

View File

@ -115,9 +115,10 @@ func TestScan(t *testing.T) {
})
t.Run("duration convert", func(t *testing.T) {
// not supported, otherwise any integer field would become convertible:
type dur time.Duration
var d dur
if !bind.BindScalar(&d, "9s") || d != dur(9*time.Second) {
if bind.BindScalar(&d, "9s") {
t.Fatal(d)
}
})

63
type.go
View File

@ -20,7 +20,7 @@ func (f unpackFlag) has(v unpackFlag) bool {
return f&v > 0
}
func scalarType(t reflect.Type) Scalar {
func scalarType(t reflect.Type) reflect.Kind {
switch t.Kind() {
case reflect.Bool:
return Bool
@ -29,13 +29,13 @@ func scalarType(t reflect.Type) Scalar {
return Duration
}
return Int
return FieldType(t.Kind())
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return Uint
return FieldType(t.Kind())
case reflect.Float32, reflect.Float64:
return Float
return FieldType(t.Kind())
case reflect.String:
return String
return FieldType(t.Kind())
default:
if isTime(t) {
return Time
@ -45,41 +45,20 @@ func scalarType(t reflect.Type) Scalar {
}
}
func scalarTypeReflect(v any) Scalar {
func scalarTypeReflect(v any) FieldType {
t := reflect.TypeOf(v)
if t == nil {
return Any
}
if hasCircularType(t) {
return Any
}
t = unpackType(t, pointer|slice)
return scalarType(t)
}
func scalarSize(t reflect.Type) int {
return int(t.Size()) * 8
}
func valueSize(v reflect.Value) int {
switch v.Kind() {
case reflect.String:
return v.Len() * 8
case reflect.Interface:
return valueSize(unpackValue(v, pointer|slice|iface|anytype))
case reflect.Invalid:
return 0
default:
return scalarSize(v.Type())
}
}
func valueSizeReflect(v any) int {
r := reflect.ValueOf(v)
if hasCircularReference(r) {
return 0
}
return valueSize(r)
}
func setVisited[T comparable](visited map[T]bool, k T) map[T]bool {
s := make(map[T]bool)
for v := range visited {
@ -191,7 +170,7 @@ func isTime(t reflect.Type) bool {
}
func isDuration(t reflect.Type) bool {
return t.ConvertibleTo(reflect.TypeFor[time.Duration]())
return t == reflect.TypeFor[time.Duration]()
}
func isScalar(t reflect.Type) bool {
@ -271,6 +250,24 @@ func unpackValue(v reflect.Value, unpack unpackFlag) reflect.Value {
return v
}
func unpackAllValues(v reflect.Value) []reflect.Value {
if !v.IsValid() {
return nil
}
v = unpackValue(v, pointer|iface|anytype)
if v.Kind() != reflect.Slice {
return []reflect.Value{v}
}
var all []reflect.Value
for i := 0; i < v.Len(); i++ {
all = append(all, unpackAllValues(v.Index(i))...)
}
return all
}
func acceptsScalar(t reflect.Type) bool {
t = unpackType(t, pointer|slice)
return isAny(t) || isScalar(t)

View File

@ -349,14 +349,6 @@ func TestTypeChecks(t *testing.T) {
}
})
t.Run("via struct field and interface", func(t *testing.T) {
var s struct{ F any }
s.F = s
if len(bind.FieldValues(s)) > 0 {
t.Fatal()
}
})
t.Run("value with circular type", func(t *testing.T) {
type p *p
var v p