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 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 == "" { 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 { if !v.IsValid() { return []Field{nilField()} } 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 !isStruct(t) { 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 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 { 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 bound bool ) 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 { 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)) { b := bindField(receiver.Field(i), trimNameAndPath(sfn, values)) bound = bound || b } } for i := 0; i < t.NumField(); i++ { sf := t.Field(i) if !sf.Anonymous { continue } b := bindField(receiver.Field(i), values) bound = bound || b } 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) } 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 isStruct(fieldReceiver.Type()) { return bindStructField(fieldReceiver, values) } if !receiver.CanSet() { return false } t := receiver.Type() ut := unpackType(t, pointer) if ut.Kind() == reflect.Slice || isScalarMap(ut) || isStruct(ut) { 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 { 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 _, pname := range pathsOrdered { group := paths[pname] nfp := nameFromPath(group[0].path) group = append(group, names[nfp]...) delete(names, nfp) groups = append(groups, group) } for _, name := range namesOrdered { groups = append(groups, names[name]) } return groups } func bindFields(receiver reflect.Value, values []Field) []Field { unmatched, try := filterFields(fieldHasCircRef, values) groups := groupFields(try) for _, g := range groups { if !bindField(receiver, g) { unmatched = append(unmatched, g...) } } return unmatched } func bindFieldsReflect(structure any, values []Field) []Field { receiver := reflect.ValueOf(structure) if hasCircularReference(receiver) { return values } if !receiver.IsValid() || !acceptsFields(receiver.Type()) { return values } return bindFields(receiver, values) } func bindFieldsCreateReflect[T any](values []Field) (T, []Field) { t := reflect.TypeFor[T]() if hasCircularType(t) { var r T return r, values } if !acceptsFields(t) { var r T return r, values } receiver, ok := allocate(t, 1) if !ok { var r T return r, values } unmatched := bindFields(receiver, values) if len(unmatched) == len(values) { receiver = reflect.Zero(t) } return receiver.Interface().(T), unmatched }