1
0

documentation

This commit is contained in:
Arpad Ryszka 2025-08-31 17:11:09 +02:00
parent cfa9b6e6f7
commit 9e0dcd6677
9 changed files with 269 additions and 128 deletions

View File

@ -47,6 +47,31 @@ func hasPath(f Field) bool {
return len(f.path) > 0 return len(f.path) > 0
} }
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 { func fields(t reflect.Type) []Field {
t = unpackType(t, pointer) t = unpackType(t, pointer)
list := t.Kind() == reflect.Slice list := t.Kind() == reflect.Slice
@ -63,13 +88,7 @@ func fields(t reflect.Type) []Field {
} }
if acceptsScalar(tfi.Type) { if acceptsScalar(tfi.Type) {
var fi Field fi := fieldFromType(tfi.Name, tfi.Type)
ft := unpackType(tfi.Type, pointer|slice)
fi.typ = scalarType(ft.Kind())
fi.size = scalarSize(ft.Kind())
fi.list = acceptsList(tfi.Type)
fi.name = strcase.ToKebab(tfi.Name)
fi.path = []string{tfi.Name}
f = append(f, fi) f = append(f, fi)
continue continue
} }
@ -102,14 +121,6 @@ func fields(t reflect.Type) []Field {
return f return f
} }
func fieldFromValue(v reflect.Value) Field {
var fi Field
fi.value = v.Interface()
fi.typ = scalarType(v.Kind())
fi.size = valueSize(v)
return fi
}
func prependFieldName(name string, f []Field) { func prependFieldName(name string, f []Field) {
for i := range f { for i := range f {
if f[i].name == "" { if f[i].name == "" {
@ -144,6 +155,10 @@ func scalarMapFields(v reflect.Value) []Field {
} }
func fieldValues(v reflect.Value) []Field { func fieldValues(v reflect.Value) []Field {
if !v.IsValid() {
return []Field{nilField()}
}
var f []Field var f []Field
v = unpackValue(v, pointer|anytype|iface) v = unpackValue(v, pointer|anytype|iface)
t := v.Type() t := v.Type()
@ -287,7 +302,6 @@ func bindMapField(receiver reflect.Value, values []Field) bool {
} }
func trimNameAndPath(name string, values []Field) []Field { func trimNameAndPath(name string, values []Field) []Field {
name = strcase.ToKebab(name)
v := make([]Field, len(values)) v := make([]Field, len(values))
copy(v, values) copy(v, values)
for i := range v { for i := range v {
@ -308,7 +322,11 @@ func trimNameAndPath(name string, values []Field) []Field {
} }
func bindStructField(receiver reflect.Value, values []Field) bool { func bindStructField(receiver reflect.Value, values []Field) bool {
var name, pathName string var (
name, pathName string
bound bool
)
fp, nfp := filterFields(hasPath, values) fp, nfp := filterFields(hasPath, values)
if len(fp) > 0 { if len(fp) > 0 {
pathName = fp[0].path[0] pathName = fp[0].path[0]
@ -326,15 +344,15 @@ func bindStructField(receiver reflect.Value, values []Field) bool {
} }
if sf.Name == pathName { if sf.Name == pathName {
values = trimNameAndPath(pathName, values) b := bindField(receiver.Field(i), trimNameAndPath(pathName, values))
return bindField(receiver.Field(i), values) bound = bound || b
continue
} }
sfn := strcase.ToKebab(sf.Name) sfn := strcase.ToKebab(sf.Name)
if name == sfn || if name == sfn || strings.HasPrefix(name, fmt.Sprintf("%s-", sfn)) {
strings.HasPrefix(name, fmt.Sprintf("%s-", sfn)) { b := bindField(receiver.Field(i), trimNameAndPath(sfn, values))
values = trimNameAndPath(sfn, values) bound = bound || b
return bindField(receiver.Field(i), values)
} }
} }
@ -344,15 +362,18 @@ func bindStructField(receiver reflect.Value, values []Field) bool {
continue continue
} }
if bindField(receiver.Field(i), values) { b := bindField(receiver.Field(i), values)
return true bound = bound || b
}
} }
return false return bound
} }
func bindField(receiver reflect.Value, values []Field) bool { func bindField(receiver reflect.Value, values []Field) bool {
if !receiver.IsValid() {
return false
}
if values[0].name == "" && len(values[0].path) == 0 { if values[0].name == "" && len(values[0].path) == 0 {
return bindScalarField(receiver, values) return bindScalarField(receiver, values)
} }
@ -420,28 +441,38 @@ func fieldValuesReflect(structure any) []Field {
} }
func groupFields(f []Field) [][]Field { func groupFields(f []Field) [][]Field {
var pathsOrdered, namesOrdered []string
withPath, withoutPath := filterFields(hasPath, f) withPath, withoutPath := filterFields(hasPath, f)
paths := make(map[string][]Field) paths := make(map[string][]Field)
for _, ff := range withPath { for _, ff := range withPath {
ps := pathString(ff) ps := pathString(ff)
if _, set := paths[ps]; !set {
pathsOrdered = append(pathsOrdered, ps)
}
paths[ps] = append(paths[ps], ff) paths[ps] = append(paths[ps], ff)
} }
names := make(map[string][]Field) names := make(map[string][]Field)
for _, ff := range withoutPath { for _, ff := range withoutPath {
if _, set := names[ff.name]; !set {
namesOrdered = append(namesOrdered, ff.name)
}
names[ff.name] = append(names[ff.name], ff) names[ff.name] = append(names[ff.name], ff)
} }
var groups [][]Field var groups [][]Field
for _, group := range paths { for _, pname := range pathsOrdered {
group := paths[pname]
nfp := nameFromPath(group[0].path) nfp := nameFromPath(group[0].path)
group = append(group, names[nfp]...) group = append(group, names[nfp]...)
delete(names, nfp) delete(names, nfp)
groups = append(groups, group) groups = append(groups, group)
} }
for _, group := range names { for _, name := range namesOrdered {
groups = append(groups, group) groups = append(groups, names[name])
} }
return groups return groups
@ -465,7 +496,7 @@ func bindFieldsReflect(structure any, values []Field) []Field {
return values return values
} }
if !acceptsFields(receiver.Type()) { if !receiver.IsValid() || !acceptsFields(receiver.Type()) {
return values return values
} }

View File

@ -5,10 +5,32 @@ import (
"code.squareroundforest.org/arpio/notation" "code.squareroundforest.org/arpio/notation"
"slices" "slices"
"sort" "sort"
"strings"
"testing" "testing"
) )
func TestField(t *testing.T) { func TestField(t *testing.T) {
sortFields := func(f []bind.Field, p ...func(int, int) bool) {
sort.Slice(f, func(i, j int) bool {
pi, pj := strings.Join(f[i].Path(), ":"), strings.Join(f[j].Path(), ":")
if pi < pj {
return true
}
if f[i].Name() < f[j].Name() {
return true
}
for _, pi := range p {
if pi(i, j) {
return true
}
}
return false
})
}
t.Run("fields", func(t *testing.T) { t.Run("fields", func(t *testing.T) {
type s0 struct { type s0 struct {
FieldOne int FieldOne int
@ -113,44 +135,22 @@ func TestField(t *testing.T) {
t.Run("scalar map", func(t *testing.T) { t.Run("scalar map", func(t *testing.T) {
f := bind.FieldValues(map[string]int{"foo": 21, "bar": 42}) f := bind.FieldValues(map[string]int{"foo": 21, "bar": 42})
sort.Slice(f, func(i, j int) bool { sortFields(f)
if f[i].Name() > f[j].Name() {
return true
}
if f[i].Value().(int) < f[j].Value().(int) {
return true
}
return false
})
if len(f) != 2 || if len(f) != 2 ||
f[0].Name() != "foo" || f[0].Value() != 21 || !f[0].Free() || f[0].Name() != "bar" || f[0].Value() != 42 || !f[0].Free() ||
f[1].Name() != "bar" || f[1].Value() != 42 || !f[1].Free() { f[1].Name() != "foo" || f[1].Value() != 21 || !f[1].Free() {
t.Fatal(notation.Sprint(f)) t.Fatal(notation.Sprint(f))
} }
}) })
t.Run("scalar map with list values", func(t *testing.T) { t.Run("scalar map with list values", func(t *testing.T) {
f := bind.FieldValues(map[string][]int{"foo": []int{21, 36}, "bar": []int{42, 72}}) f := bind.FieldValues(map[string][]int{"foo": []int{21, 36}, "bar": []int{42, 72}})
sort.Slice(f, func(i, j int) bool { sortFields(f)
if f[i].Name() > f[j].Name() {
return true
}
if f[i].Value().(int) < f[j].Value().(int) {
return true
}
return false
})
if len(f) != 4 || if len(f) != 4 ||
f[0].Name() != "foo" || f[0].Value() != 21 || !f[0].Free() || f[0].Name() != "bar" || f[0].Value() != 42 || !f[0].Free() ||
f[1].Name() != "foo" || f[1].Value() != 36 || !f[1].Free() || f[1].Name() != "bar" || f[1].Value() != 72 || !f[1].Free() ||
f[2].Name() != "bar" || f[2].Value() != 42 || !f[2].Free() || f[2].Name() != "foo" || f[2].Value() != 21 || !f[2].Free() ||
f[3].Name() != "bar" || f[3].Value() != 72 || !f[3].Free() { f[3].Name() != "foo" || f[3].Value() != 36 || !f[3].Free() {
t.Fatal(notation.Sprint(f)) t.Fatal(notation.Sprint(f))
} }
}) })
@ -614,6 +614,7 @@ func TestField(t *testing.T) {
Bar *struct{ Baz *string } Bar *struct{ Baz *string }
Qux *[]struct{ Quux string } Qux *[]struct{ Quux string }
} }
var v s var v s
u := bind.BindFields( u := bind.BindFields(
&v, &v,
@ -632,6 +633,7 @@ func TestField(t *testing.T) {
Foo *int Foo *int
Bar *chan int Bar *chan int
} }
var v s var v s
u := bind.BindFields( u := bind.BindFields(
&v, &v,
@ -640,8 +642,9 @@ func TestField(t *testing.T) {
bind.NamedValue("bar-baz", "corge"), bind.NamedValue("bar-baz", "corge"),
) )
sortFields(u)
if len(u) != 2 || u[0].Name() != "bar" || u[1].Name() != "bar-baz" || *v.Foo != 42 { if len(u) != 2 || u[0].Name() != "bar" || u[1].Name() != "bar-baz" || *v.Foo != 42 {
t.Fatal() t.Fatal(notation.Sprint(u), notation.Sprint(v))
} }
}) })
@ -700,6 +703,43 @@ func TestField(t *testing.T) {
t.Fatal() t.Fatal()
} }
}) })
t.Run("empty receiver", func(t *testing.T) {
var v any
u := bind.BindFields(&v, bind.NamedValue("foo", 42))
if len(u) != 1 {
t.Fatal()
}
})
t.Run("nil value", func(t *testing.T) {
type s struct{ Foo any }
v := s{42}
u := bind.BindFields(&v, bind.NamedValue("foo", nil))
if len(u) != 0 || v.Foo != nil {
t.Fatal()
}
})
t.Run("nil receiver", func(t *testing.T) {
u := bind.BindFields(nil, bind.NamedValue("foo", nil))
if len(u) != 1 {
t.Fatal()
}
})
t.Run("ambigously named fields", func(t *testing.T) {
type s struct {
FooBar int
Foo struct{ Bar int }
}
var v s
u := bind.BindFields(&v, bind.NamedValue("foo-bar", 42))
if len(u) != 0 || v.FooBar != 42 || v.Foo.Bar != 42 {
t.Fatal(notation.Sprint(u), notation.Sprint(v))
}
})
}) })
t.Run("bind fields create", func(t *testing.T) { t.Run("bind fields create", func(t *testing.T) {
@ -736,5 +776,12 @@ func TestField(t *testing.T) {
t.Fatal() t.Fatal()
} }
}) })
t.Run("any type", func(t *testing.T) {
_, u := bind.BindFieldsCreate[any](bind.NamedValue("foo", 42))
if len(u) != 1 {
t.Fatal()
}
})
}) })
} }

79
lib.go
View File

@ -1,10 +1,7 @@
// provides more flexible and permissive ways of setting values than reflect.Value.Set // Package bind provides functions to work with flattened lists of structure fields.
// circular type structures not supported
// it handles scalar fields
// primary use cases by design are: command line options, environment variables, ini file fields, URL query parameters, HTTP form values
// traverses pointers, slices, wrapper interfaces
package bind package bind
// Scalar defines scalar types that can be used as field values and are not traversed.
type Scalar int type Scalar int
const ( const (
@ -14,9 +11,14 @@ const (
Uint Uint
Float Float
String String
Duration
Time
) )
// Field is a field of structure or a compatible map. It is used as the output of the inspection functions, and
// as the input of the binding functions.
type Field struct { type Field struct {
input bool
path []string path []string
name string name string
list bool list bool
@ -26,29 +28,43 @@ type Field struct {
value any value any
} }
// the receiver must be addressable // BindScalar sets the receiver to the provided value or values. It traverses the receiver through wrappers like
// pointers, slices and interfaces. If the receiver contains a slice which is greater or equal length as the
// number of the provided values, N, it sets the first N slice items to the values. It returns true, if the
// values were set. It returns false, if the values were not set, e.g. due to incompatible types.
func BindScalar(receiver any, value ...any) bool { func BindScalar(receiver any, value ...any) bool {
return bindScalarReflect(receiver, value) return bindScalarReflect(receiver, value)
} }
// BindScalarCreate is like BindScalar, but it allocates the receiver from the type T.
func BindScalarCreate[T any](value ...any) (T, bool) { func BindScalarCreate[T any](value ...any) (T, bool) {
return bindScalarCreateReflect[T](value) return bindScalarCreateReflect[T](value)
} }
// 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 { func ValueByPath(path []string, value any) Field {
return Field{path: path, value: value} return Field{input: true, path: path, value: value}
} }
// NamedValue defines a field for inptu to BindFields or BindFieldsCreate. The name is the kebab case
// representation of the field path. The struct boundaries in the field path are not represented in the name,
// which leads to potentially ambigous field references, depending on the receiver type structure and naming. In
// case of ambigous fields, the field that can match multiple structure fields, will be bound to each matched
// structure field.
func NamedValue(name string, value any) Field { func NamedValue(name string, value any) Field {
return Field{name: name, value: value} return Field{input: true, name: name, value: value}
} }
// Path returns the structure path of a field.
func (f Field) Path() []string { func (f Field) Path() []string {
p := make([]string, len(f.path)) p := make([]string, len(f.path))
copy(p, f.path) copy(p, f.path)
return p return p
} }
// Name returns the structure path of a field concatenated and in kebab casing, unless defined otherwise by
// NamedValue.
func (f Field) Name() string { func (f Field) Name() string {
if f.name != "" || len(f.path) == 0 { if f.name != "" || len(f.path) == 0 {
return f.name return f.name
@ -57,43 +73,76 @@ func (f Field) Name() string {
return nameFromPath(f.path) return nameFromPath(f.path)
} }
// List indicates whether a field is wrapped in a slice and accpets multiple values.
func (f Field) List() bool { return f.list } func (f Field) List() bool { return f.list }
func (f Field) Type() Scalar { return f.typ }
func (f Field) Size() int { return f.size }
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 // Type returns the scalar type of the field.
// non-struct and non-named map values return unnamed fields func (f Field) Type() Scalar {
if !f.input {
return f.typ
}
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
}
// Value returns the value of a field.
func (f Field) Value() any {
return f.value
}
// Fields returns the fields of a structure type recursively. It traverses through pointers and slices.
func Fields[T any]() []Field { func Fields[T any]() []Field {
return fieldsReflect[T]() return fieldsReflect[T]()
} }
// the list and bool flags are not set because it is not possible if they are defined by the root type // FieldValues returns the fields of a structure value recursively. It traverses through pointers, slices and
// interfaces.
func FieldValues(structure any) []Field { func FieldValues(structure any) []Field {
return fieldValuesReflect(structure) return fieldValuesReflect(structure)
} }
// BindFields sets structure fields recursively. It traverses through poitners, slices and interfaces. It
// returns the values for which it is not possible to find a compatible matching field. It supports maps that
// have string keys and scalar values.
func BindFields(structure any, values ...Field) []Field { func BindFields(structure any, values ...Field) []Field {
return bindFieldsReflect(structure, values) return bindFieldsReflect(structure, values)
} }
// BindFieldsCreate is like BindFields, but it allocates the receiver from type T.
func BindFieldsCreate[T any](values ...Field) (T, []Field) { func BindFieldsCreate[T any](values ...Field) (T, []Field) {
return bindFieldsCreateReflect[T](values) return bindFieldsCreateReflect[T](values)
} }
// AcceptsScalar checks if a type can be used with BindScalarCreate or the values of the type with BindScalar.
func AcceptsScalar[T any]() bool { func AcceptsScalar[T any]() bool {
return acceptsScalarReflect[T]() return acceptsScalarReflect[T]()
} }
// AcceptsFields checks if a type can be used with BindFieldsCreate or the values of the type with BindFields.
func AcceptsFields[T any]() bool { func AcceptsFields[T any]() bool {
return acceptsFieldsReflect[T]() return acceptsFieldsReflect[T]()
} }
// AcceptsList checks if a type can be used to bind multiple values.
func AcceptsList[T any]() bool { func AcceptsList[T any]() bool {
return acceptsListReflect[T]() return acceptsListReflect[T]()
} }
// Bindable is the same as AcceptsScalar[T]() || AcceptsFields[T]().
func Bindable[T any]() bool { func Bindable[T any]() bool {
return bindableReflect[T]() return bindableReflect[T]()
} }

View File

@ -1,7 +0,0 @@
add the kind to the field type but with time and duration support. Simplified for ints, uints and floats, with bit size. Skipping the unused ones. With different name, e.g. ScalarType
track down the cases when reflect can panic
documentation:
- give a short description for every exported symbol
- start from collecting the docs from the test cases
- extrace the common doc items from the function doc items
doc/test/code triangle

7
readme.md Normal file
View File

@ -0,0 +1,7 @@
# Bind
Package bind provides functions to work with flattened lists of structure fields.
See documentation at: https://godocs.io/code.squareroundforest.org/arpio/bind
*Made in Berlin, DE*

View File

@ -3,7 +3,7 @@ package bind
import "reflect" import "reflect"
func bindScalar(receiver reflect.Value, values []any) bool { func bindScalar(receiver reflect.Value, values []any) bool {
if len(values) == 0 { if !receiver.IsValid() || len(values) == 0 {
return false return false
} }
@ -13,14 +13,23 @@ func bindScalar(receiver reflect.Value, values []any) bool {
if len(values) == 1 { if len(values) == 1 {
receiver = unpackValue(receiver, pointer|iface|slice) receiver = unpackValue(receiver, pointer|iface|slice)
r := reflect.ValueOf(values[0]) if !receiver.CanSet() {
r = unpackValue(r, pointer|iface|slice)
v, ok := scan(receiver.Type(), r.Interface())
if !ok {
return false return false
} }
if !receiver.CanSet() { r := reflect.ValueOf(values[0])
r = unpackValue(r, pointer|iface|slice)
if !r.IsValid() && !isAny(receiver.Type()) {
return false
}
if !r.IsValid() {
receiver.Set(reflect.Zero(receiver.Type()))
return true
}
v, ok := scan(receiver.Type(), r.Interface())
if !ok {
return false return false
} }

View File

@ -156,6 +156,20 @@ func TestScalar(t *testing.T) {
t.Fatal() t.Fatal()
} }
}) })
t.Run("nil value", func(t *testing.T) {
var v any
v = 42
if !bind.BindScalar(&v, nil) || v != nil {
t.Fatal()
}
})
t.Run("nil receiver", func(t *testing.T) {
if bind.BindScalar(nil, 42) {
t.Fatal()
}
})
}) })
t.Run("bind scalar with create", func(t *testing.T) { t.Run("bind scalar with create", func(t *testing.T) {

View File

@ -54,6 +54,10 @@ func parseUint(s string, byteSize int) (uint64, error) {
} }
func scanConvert(t reflect.Type, v any) (any, bool) { func scanConvert(t reflect.Type, v any) (any, bool) {
if isAny(t) {
return v, true
}
r := reflect.ValueOf(v) r := reflect.ValueOf(v)
if !r.CanConvert(t) { if !r.CanConvert(t) {
return nil, false return nil, false

61
type.go
View File

@ -20,11 +20,15 @@ func (f unpackFlag) has(v unpackFlag) bool {
return f&v > 0 return f&v > 0
} }
func scalarType(k reflect.Kind) Scalar { func scalarType(t reflect.Type) Scalar {
switch k { switch t.Kind() {
case reflect.Bool: case reflect.Bool:
return Bool return Bool
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) {
return Duration
}
return Int return Int
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return Uint return Uint
@ -33,43 +37,20 @@ func scalarType(k reflect.Kind) Scalar {
case reflect.String: case reflect.String:
return String return String
default: default:
if isTime(t) {
return Time
}
return Any return Any
} }
} }
func scalarSize(k reflect.Kind) int { func scalarTypeReflect(v any) Scalar {
switch k { return scalarType(reflect.TypeOf(v))
case reflect.Bool: }
return 1
case reflect.Int: func scalarSize(t reflect.Type) int {
return int(reflect.TypeFor[int]().Size()) * 8 return int(t.Size()) * 8
case reflect.Int8:
return 8
case reflect.Int16:
return 16
case reflect.Int32:
return 32
case reflect.Int64:
return 64
case reflect.Uint:
return int(reflect.TypeFor[uint]().Size()) * 8
case reflect.Uint8:
return 8
case reflect.Uint16:
return 16
case reflect.Uint32:
return 32
case reflect.Uint64:
return 64
case reflect.Float32:
return 32
case reflect.Float64:
return 64
case reflect.String:
return -1
default:
return -1
}
} }
func valueSize(v reflect.Value) int { func valueSize(v reflect.Value) int {
@ -78,11 +59,17 @@ func valueSize(v reflect.Value) int {
return v.Len() * 8 return v.Len() * 8
case reflect.Interface: case reflect.Interface:
return valueSize(unpackValue(v, pointer|slice|iface|anytype)) return valueSize(unpackValue(v, pointer|slice|iface|anytype))
case reflect.Invalid:
return 0
default: default:
return scalarSize(v.Kind()) return scalarSize(v.Type())
} }
} }
func valueSizeReflect(v any) int {
return valueSize(reflect.ValueOf(v))
}
func setVisited[T comparable](visited map[T]bool, k T) map[T]bool { func setVisited[T comparable](visited map[T]bool, k T) map[T]bool {
s := make(map[T]bool) s := make(map[T]bool)
for v := range visited { for v := range visited {
@ -247,7 +234,7 @@ func unpackType(t reflect.Type, unpack unpackFlag) reflect.Type {
} }
func unpackValue(v reflect.Value, unpack unpackFlag) reflect.Value { func unpackValue(v reflect.Value, unpack unpackFlag) reflect.Value {
if v.IsZero() { if !v.IsValid() || v.IsZero() {
return v return v
} }