1
0
bind/field.go
2025-08-31 01:23:21 +02:00

468 lines
8.3 KiB
Go

package bind
import (
"fmt"
"github.com/iancoleman/strcase"
"reflect"
"strings"
"unicode"
)
func pathString(f Field) string {
return strings.Join(f.path, ":")
}
func nameFromPath(p []string) string {
var pp []string
for _, pi := range p {
pp = append(pp, strcase.ToKebab(pi))
}
return strings.Join(pp, "-")
}
func exported(name string) bool {
return unicode.IsUpper([]rune(name)[0])
}
func filterFields(predicate func(Field) bool, f []Field) ([]Field, []Field) {
var yes, no []Field
for _, fi := range f {
if predicate(fi) {
yes = append(yes, fi)
continue
}
no = append(no, fi)
}
return yes, no
}
func fieldHasCircRef(f Field) bool {
return hasCircularReference(reflect.ValueOf(f.Value()))
}
func hasPath(f Field) bool {
return len(f.path) > 0
}
func fields(t reflect.Type) []Field {
t = unpackType(t, pointer)
list := t.Kind() == reflect.Slice
t = unpackType(t, pointer|slice)
if t.Kind() != reflect.Struct {
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) {
var fi Field
ft := unpackType(tfi.Type, pointer|slice)
fi.isBool = ft.Kind() == reflect.Bool
fi.list = acceptsList(tfi.Type)
fi.name = strcase.ToKebab(tfi.Name)
fi.path = []string{tfi.Name}
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 fieldFromValue(v reflect.Value) Field {
var fi Field
fi.value = v.Interface()
fi.isBool = v.Kind() == reflect.Bool
return fi
}
func prependFieldName(name string, f []Field) {
for i := range f {
if f[i].name == "" {
f[i].name = strcase.ToKebab(name)
f[i].path = []string{name}
continue
}
f[i].name = fmt.Sprintf("%s-%s", strcase.ToKebab(name), f[i].name)
f[i].path = append([]string{name}, f[i].path...)
}
}
func freeFields(f []Field) {
for i := range f {
f[i].free = true
}
}
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)
freeFields(fk)
f = append(f, fk...)
}
return f
}
func fieldValues(v reflect.Value) []Field {
var f []Field
v = unpackValue(v, pointer|anytype|iface)
t := v.Type()
if isScalar(t) {
return []Field{fieldFromValue(v)}
}
if v.Kind() == reflect.Slice {
for i := 0; i < v.Len(); i++ {
f = append(f, fieldValues(v.Index(i))...)
}
return f
}
if isScalarMap(t) {
return scalarMapFields(v)
}
if t.Kind() != reflect.Struct {
return nil
}
for i := 0; i < t.NumField(); i++ {
tfi := t.Field(i)
if !exported(tfi.Name) && !tfi.Anonymous {
continue
}
vfi := v.Field(i)
vfi = unpackValue(vfi, pointer|iface|anytype)
switch {
case vfi.Kind() == reflect.Struct && tfi.Anonymous:
ff := fieldValues(vfi)
f = append(f, ff...)
default:
ff := fieldValues(vfi)
prependFieldName(tfi.Name, ff)
f = append(f, ff...)
}
}
return f
}
func takeFieldValues(f []Field) []any {
var v []any
for _, fi := range f {
v = append(v, fi.Value())
}
return v
}
func bindScalarField(receiver reflect.Value, values []Field) bool {
v := takeFieldValues(values)
if !receiver.CanSet() {
return bindScalar(receiver, v)
}
rv, ok := allocate(receiver.Type(), len(v))
if !ok {
return false
}
if ok = bindScalar(rv, v); ok {
receiver.Set(rv)
}
return ok
}
func bindListField(receiver reflect.Value, values []Field) bool {
if receiver.Len() < len(values) && !receiver.CanSet() {
return false
}
if receiver.Len() < len(values) {
newList, ok := allocate(receiver.Type(), len(values))
if !ok {
return false
}
reflect.Copy(newList, receiver)
receiver.Set(newList)
}
for i := range values {
if !bindField(receiver.Index(i), values[i:i+1]) {
return false
}
}
return true
}
func bindMapField(receiver reflect.Value, values []Field) bool {
for _, v := range values {
if len(v.path) > 1 {
return false
}
}
if receiver.IsZero() && !receiver.CanSet() {
return false
}
var key string
fp, nfp := filterFields(hasPath, values)
if len(fp) > 0 {
key = fp[0].path[0]
} else {
key = nfp[0].name
}
v := takeFieldValues(values)
t := receiver.Type()
kt := t.Key()
vt := t.Elem()
kv, ok := bindScalarCreate(kt, []any{key})
if !ok {
return false
}
vv, ok := bindScalarCreate(vt, v)
if !ok {
return false
}
if receiver.IsZero() {
rv, ok := allocate(receiver.Type(), 1)
if !ok {
return false
}
receiver.Set(rv)
}
receiver.SetMapIndex(kv, vv)
return true
}
func trimNameAndPath(name string, values []Field) []Field {
name = strcase.ToKebab(name)
v := make([]Field, len(values))
copy(v, values)
for i := range v {
if len(v[i].path) > 0 {
v[i].path = v[i].path[1:]
}
if v[i].name == name {
v[i].name = ""
}
if strings.HasPrefix(v[i].name, fmt.Sprintf("%s-", name)) {
v[i].name = v[i].name[len(name)+1:]
}
}
return v
}
func bindStructField(receiver reflect.Value, values []Field) bool {
var name, pathName string
fp, nfp := filterFields(hasPath, values)
if len(fp) > 0 {
pathName = fp[0].path[0]
}
if len(nfp) > 0 {
name = nfp[0].name
}
t := receiver.Type()
for i := 0; i < t.NumField(); i++ {
sf := t.Field(i)
if sf.Anonymous {
continue
}
if sf.Name == pathName {
values = trimNameAndPath(pathName, values)
return bindField(receiver.Field(i), values)
}
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)
}
}
for i := 0; i < t.NumField(); i++ {
sf := t.Field(i)
if !sf.Anonymous {
continue
}
if bindField(receiver.Field(i), values) {
return true
}
}
return false
}
func bindField(receiver reflect.Value, values []Field) bool {
if values[0].name == "" && len(values[0].path) == 0 {
return bindScalarField(receiver, values)
}
listReceiver := unpackValue(receiver, pointer|iface|anytype)
if listReceiver.Kind() == reflect.Slice {
return bindListField(listReceiver, values)
}
fieldReceiver := unpackValue(receiver, pointer|slice|iface)
if isScalarMap(fieldReceiver.Type()) {
return bindMapField(fieldReceiver, values)
}
if fieldReceiver.Kind() == reflect.Struct {
return bindStructField(fieldReceiver, values)
}
if !receiver.CanSet() {
return false
}
t := receiver.Type()
ut := unpackType(t, pointer)
if ut.Kind() == reflect.Slice ||
isScalarMap(ut) ||
ut.Kind() == reflect.Struct {
l := 1
if ut.Kind() == reflect.Slice {
l = len(values)
}
rv, ok := allocate(t, l)
if !ok {
return false
}
if !bindField(rv, values) {
return false
}
receiver.Set(rv)
return true
}
return false
}
func fieldsReflect[T any]() []Field {
t := reflect.TypeFor[T]()
if hasCircularType(t) {
return nil
}
return fields(t)
}
func fieldValuesReflect(structure any) []Field {
v := reflect.ValueOf(structure)
if hasCircularReference(v) {
return nil
}
return fieldValues(v)
}
func groupFields(f []Field) [][]Field {
withPath, withoutPath := filterFields(hasPath, f)
paths := make(map[string][]Field)
for _, ff := range withPath {
ps := pathString(ff)
paths[ps] = append(paths[ps], ff)
}
names := make(map[string][]Field)
for _, ff := range withoutPath {
names[ff.name] = append(names[ff.name], ff)
}
var groups [][]Field
for _, group := range paths {
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)
}
return groups
}
func bindFieldsReflect(structure any, values []Field) []Field {
receiver := reflect.ValueOf(structure)
if hasCircularReference(receiver) {
return values
}
if !acceptsFields(receiver.Type()) {
return values
}
unmatched, try := filterFields(fieldHasCircRef, values)
groups := groupFields(try)
for _, g := range groups {
if !bindField(receiver, g) {
unmatched = append(unmatched, g...)
}
}
return unmatched
}