Files
path/path.go
Ian Gulliver f50e6e228c Module split
2023-04-20 18:01:36 +00:00

260 lines
5.2 KiB
Go

package path
import (
"errors"
"fmt"
"reflect"
"sort"
"strings"
"time"
"cloud.google.com/go/civil"
"github.com/gopatchy/jsrest"
"golang.org/x/exp/slices"
)
type WalkCallback func(string, []string, reflect.StructField)
var (
TimeTimeType = reflect.TypeOf(time.Time{})
CivilDateType = reflect.TypeOf(civil.Date{})
ErrNotAStruct = errors.New("not a struct")
ErrUnknownFieldName = errors.New("unknown field name")
)
func Get(obj any, path string) (any, error) {
v, err := GetValue(reflect.ValueOf(obj), path)
if err != nil {
return nil, err
}
return v.Interface(), nil
}
func GetValue(v reflect.Value, path string) (reflect.Value, error) {
parts := strings.Split(path, ".")
return getRecursive(v, parts, []string{})
}
func getRecursive(v reflect.Value, parts []string, prev []string) (reflect.Value, error) {
if v.Kind() == reflect.Pointer {
if v.IsNil() {
v = reflect.Zero(v.Type().Elem())
} else {
v = reflect.Indirect(v)
}
}
if len(parts) == 0 {
return v, nil
}
if v.Kind() != reflect.Struct {
return reflect.Value{}, jsrest.Errorf(jsrest.ErrBadRequest, "%s (%w)", strings.Join(prev, "."), ErrNotAStruct)
}
part := parts[0]
sub, found := getField(v, part)
if !found {
return reflect.Value{}, jsrest.Errorf(jsrest.ErrBadRequest, "%s (%w)", errorPath(prev, part), ErrUnknownFieldName)
}
newPrev := []string{}
newPrev = append(newPrev, prev...)
newPrev = append(newPrev, part)
return getRecursive(sub, parts[1:], newPrev)
}
func getField(v reflect.Value, name string) (reflect.Value, bool) {
field, found := getStructField(v.Type(), name)
if !found {
return reflect.Value{}, false
}
return v.FieldByName(field.Name), true
}
func Set(obj any, path string, val string) error {
return SetValue(reflect.ValueOf(obj), path, val)
}
func SetValue(v reflect.Value, path string, val string) error {
parts := strings.Split(path, ".")
return setRecursive(v, parts, []string{}, val)
}
func setRecursive(v reflect.Value, parts []string, prev []string, val string) error {
if v.Kind() == reflect.Pointer {
if v.IsNil() {
v.Set(reflect.New(v.Type().Elem()))
}
v = reflect.Indirect(v)
}
if len(parts) == 0 {
n, err := parse(val, v.Interface())
if err != nil {
return err
}
if _, ok := n.(*timeVal); ok {
n = n.(*timeVal).time
}
v.Set(reflect.ValueOf(n))
return nil
}
if v.Kind() != reflect.Struct {
return jsrest.Errorf(jsrest.ErrBadRequest, "%s (%w)", strings.Join(prev, "."), ErrNotAStruct)
}
part := parts[0]
sub, found := getField(v, part)
if !found {
return jsrest.Errorf(jsrest.ErrBadRequest, "%s (%w)", errorPath(prev, part), ErrUnknownFieldName)
}
newPrev := []string{}
newPrev = append(newPrev, prev...)
newPrev = append(newPrev, part)
return setRecursive(sub, parts[1:], newPrev, val)
}
func List(obj any) []string {
return ListType(reflect.TypeOf(obj))
}
func ListType(t reflect.Type) []string {
list := []string{}
WalkType(t, func(path string, _ []string, field reflect.StructField) {
t := MaybeIndirectType(field.Type)
if t.Kind() == reflect.Struct && t != TimeTimeType && t != CivilDateType {
return
}
list = append(list, path)
})
sort.Strings(list)
return list
}
func GetFieldType(t reflect.Type, path string) reflect.Type {
parts := strings.Split(path, ".")
for _, part := range parts {
field, found := getStructField(MaybeIndirectType(t), part)
if !found {
return nil
}
t = field.Type
}
return t
}
func FindTagValueType(t reflect.Type, key, value string) (string, bool) {
ret := ""
WalkType(t, func(path string, _ []string, field reflect.StructField) {
tag, found := field.Tag.Lookup(key)
if !found {
return
}
parts := strings.Split(tag, ",")
if slices.Contains(parts, value) {
ret = path
}
})
return ret, ret != ""
}
func Walk(obj any, cb WalkCallback) {
WalkType(reflect.TypeOf(obj), cb)
}
func WalkType(t reflect.Type, cb WalkCallback) {
walkRecursive(MaybeIndirectType(t), cb, []string{})
}
func walkRecursive(t reflect.Type, cb WalkCallback, prev []string) {
for i := 0; i < t.NumField(); i++ {
sub := t.Field(i)
newPrev := []string{}
newPrev = append(newPrev, prev...)
if !sub.Anonymous {
newPrev = append(newPrev, FieldName(sub))
}
t := MaybeIndirectType(sub.Type)
if len(newPrev) > 0 {
cb(strings.Join(newPrev, "."), newPrev, sub)
}
if t.Kind() == reflect.Struct && t != TimeTimeType && t != CivilDateType {
walkRecursive(t, cb, newPrev)
}
}
}
func getStructField(t reflect.Type, name string) (reflect.StructField, bool) {
name = strings.ToLower(name)
return t.FieldByNameFunc(func(iterName string) bool {
iterField, iterOK := t.FieldByName(iterName)
if !iterOK {
panic(iterName)
}
return strings.ToLower(FieldName(iterField)) == name
})
}
func errorPath(prev []string, part string) string {
if len(prev) == 0 {
return part
}
return fmt.Sprintf("%s.%s", strings.Join(prev, "."), part)
}
func FieldName(field reflect.StructField) string {
tag := field.Tag.Get("json")
if tag != "" {
if tag == "-" {
return ""
}
parts := strings.SplitN(tag, ",", 2)
return parts[0]
}
return field.Name
}
func MaybeIndirectType(t reflect.Type) reflect.Type {
if t.Kind() == reflect.Pointer {
return t.Elem()
}
return t
}