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

View File

@ -5,10 +5,32 @@ import (
"code.squareroundforest.org/arpio/notation"
"slices"
"sort"
"strings"
"testing"
)
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) {
type s0 struct {
FieldOne int
@ -113,44 +135,22 @@ func TestField(t *testing.T) {
t.Run("scalar map", func(t *testing.T) {
f := bind.FieldValues(map[string]int{"foo": 21, "bar": 42})
sort.Slice(f, func(i, j int) bool {
if f[i].Name() > f[j].Name() {
return true
}
if f[i].Value().(int) < f[j].Value().(int) {
return true
}
return false
})
sortFields(f)
if len(f) != 2 ||
f[0].Name() != "foo" || f[0].Value() != 21 || !f[0].Free() ||
f[1].Name() != "bar" || f[1].Value() != 42 || !f[1].Free() {
f[0].Name() != "bar" || f[0].Value() != 42 || !f[0].Free() ||
f[1].Name() != "foo" || f[1].Value() != 21 || !f[1].Free() {
t.Fatal(notation.Sprint(f))
}
})
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}})
sort.Slice(f, func(i, j int) bool {
if f[i].Name() > f[j].Name() {
return true
}
if f[i].Value().(int) < f[j].Value().(int) {
return true
}
return false
})
sortFields(f)
if len(f) != 4 ||
f[0].Name() != "foo" || f[0].Value() != 21 || !f[0].Free() ||
f[1].Name() != "foo" || f[1].Value() != 36 || !f[1].Free() ||
f[2].Name() != "bar" || f[2].Value() != 42 || !f[2].Free() ||
f[3].Name() != "bar" || f[3].Value() != 72 || !f[3].Free() {
f[0].Name() != "bar" || f[0].Value() != 42 || !f[0].Free() ||
f[1].Name() != "bar" || f[1].Value() != 72 || !f[1].Free() ||
f[2].Name() != "foo" || f[2].Value() != 21 || !f[2].Free() ||
f[3].Name() != "foo" || f[3].Value() != 36 || !f[3].Free() {
t.Fatal(notation.Sprint(f))
}
})
@ -614,6 +614,7 @@ func TestField(t *testing.T) {
Bar *struct{ Baz *string }
Qux *[]struct{ Quux string }
}
var v s
u := bind.BindFields(
&v,
@ -632,6 +633,7 @@ func TestField(t *testing.T) {
Foo *int
Bar *chan int
}
var v s
u := bind.BindFields(
&v,
@ -640,8 +642,9 @@ func TestField(t *testing.T) {
bind.NamedValue("bar-baz", "corge"),
)
sortFields(u)
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.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) {
@ -736,5 +776,12 @@ func TestField(t *testing.T) {
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
// 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 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
const (
@ -14,9 +11,14 @@ const (
Uint
Float
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 {
input bool
path []string
name string
list bool
@ -26,29 +28,43 @@ type Field struct {
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 {
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) {
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 {
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 {
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 {
p := make([]string, len(f.path))
copy(p, f.path)
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 {
if f.name != "" || len(f.path) == 0 {
return f.name
@ -57,43 +73,76 @@ func (f Field) Name() string {
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) 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
// non-struct and non-named map values return unnamed fields
// Type returns the scalar type of the field.
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 {
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 {
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 {
return bindFieldsReflect(structure, values)
}
// BindFieldsCreate is like BindFields, but it allocates the receiver from type T.
func BindFieldsCreate[T any](values ...Field) (T, []Field) {
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 {
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 {
return acceptsFieldsReflect[T]()
}
// AcceptsList checks if a type can be used to bind multiple values.
func AcceptsList[T any]() bool {
return acceptsListReflect[T]()
}
// Bindable is the same as AcceptsScalar[T]() || AcceptsFields[T]().
func Bindable[T any]() bool {
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"
func bindScalar(receiver reflect.Value, values []any) bool {
if len(values) == 0 {
if !receiver.IsValid() || len(values) == 0 {
return false
}
@ -13,14 +13,23 @@ func bindScalar(receiver reflect.Value, values []any) bool {
if len(values) == 1 {
receiver = unpackValue(receiver, pointer|iface|slice)
r := reflect.ValueOf(values[0])
r = unpackValue(r, pointer|iface|slice)
v, ok := scan(receiver.Type(), r.Interface())
if !ok {
if !receiver.CanSet() {
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
}

View File

@ -156,6 +156,20 @@ func TestScalar(t *testing.T) {
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) {

View File

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

61
type.go
View File

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