From 4c2531f4ccd216bac97d93a34b7defb0a96278d0 Mon Sep 17 00:00:00 2001 From: Inhere Date: Fri, 1 Sep 2023 13:28:41 +0800 Subject: [PATCH] :sparkles: feat: reflects - add quick call func by reflection: Call, Call2, SafeCall - and add more util func: IsIntLike, CanBeNil --- reflects/func.go | 295 ++++++++++++++++++++++++++++++++++++++++++ reflects/func_test.go | 60 +++++++++ reflects/reflects.go | 29 ++--- reflects/util.go | 22 ++++ 4 files changed, 390 insertions(+), 16 deletions(-) create mode 100644 reflects/func.go create mode 100644 reflects/func_test.go diff --git a/reflects/func.go b/reflects/func.go new file mode 100644 index 000000000..b0578cad3 --- /dev/null +++ b/reflects/func.go @@ -0,0 +1,295 @@ +package reflects + +import ( + "fmt" + "reflect" + + "github.com/gookit/goutil/basefn" + "github.com/pkg/errors" +) + +// FuncX wrap a go func. represent a function +type FuncX struct { + CallOpt + // Name of func. eg: "MyFunc" + Name string + // rv is the `reflect.Value` of func + rv reflect.Value + rt reflect.Type +} + +// NewFunc instance. param fn support func and reflect.Value +func NewFunc(fn any) *FuncX { + var ok bool + var rv reflect.Value + if rv, ok = fn.(reflect.Value); !ok { + rv = reflect.ValueOf(fn) + } + + rv = indirectInterface(rv) + if !rv.IsValid() { + panic("input func is nil") + } + + typ := rv.Type() + if typ.Kind() != reflect.Func { + basefn.Panicf("non-function of type: %s", typ) + } + + return &FuncX{rv: rv, rt: typ} +} + +// NumIn get the number of func input args +func (f *FuncX) NumIn() int { + return f.rt.NumIn() +} + +// NumOut get the number of func output args +func (f *FuncX) NumOut() int { + return f.rt.NumOut() +} + +// Call the function with given arguments. +// +// Usage: +// +// func main() { +// fn := func(a, b int) int { +// return a + b +// } +// +// fx := NewFunc(fn) +// ret, err := fx.Call(1, 2) +// fmt.Println(ret[0], err) // Output: 3 +// } +func (f *FuncX) Call(args ...any) ([]any, error) { + // convert args to []reflect.Value + argRvs := make([]reflect.Value, len(args)) + for i, arg := range args { + argRvs[i] = reflect.ValueOf(arg) + } + + ret, err := f.CallRV(argRvs) + if err != nil { + return nil, err + } + + // convert ret to []any + rets := make([]any, len(ret)) + for i, r := range ret { + rets[i] = r.Interface() + } + return rets, nil +} + +// CallRV call the function with given reflect.Value arguments. +func (f *FuncX) CallRV(args []reflect.Value) ([]reflect.Value, error) { + return Call(f.rv, args, &f.CallOpt) +} + +// String of func +func (f *FuncX) String() string { + return f.rt.String() +} + +// CallOpt call options +type CallOpt struct { + // TypeChecker check func type before call func. eg: check return values + TypeChecker func(typ reflect.Type) error + // AutoConvert try auto convert args to func args type + AutoConvert bool +} + +// OneOrTwoOutChecker check func type. only allow 1 or 2 return values +// +// Allow func returns: +// - 1 return: (value) +// - 2 return: (value, error) +var OneOrTwoOutChecker = func(typ reflect.Type) error { + if !good1or2outFunc(typ) { + return errors.New("func allow with 1 result or 2 results where the second is an error") + } + return nil +} + +// +// TIP: +// flow func refer from text/template package. +// +// + +// reports whether the function or method has the right result signature. +func good1or2outFunc(typ reflect.Type) bool { + // We allow functions with 1 result or 2 results where the second is an error. + switch { + case typ.NumOut() == 1: + return true + case typ.NumOut() == 2 && typ.Out(1) == errorType: + return true + } + return false +} + +// Call2 returns the result of evaluating the first argument as a function. +// The function must return 1 result, or 2 results, the second of which is an error. +// +// will check args and try convert input args to func args type. +// +// NOTE: Only support func with 1 or 2 return values: (val) OR (val, err) +// +// from text/template/funcs.go#call +func Call2(fn reflect.Value, args []reflect.Value) (reflect.Value, error) { + ret, err := Call(fn, args, &CallOpt{ + TypeChecker: OneOrTwoOutChecker, + }) + if err != nil { + return emptyValue, err + } + + // func return like: (val, err) + if len(ret) == 2 && !ret[1].IsNil() { + return ret[0], ret[1].Interface().(error) + } + return ret[0], nil +} + +// Call returns the result of evaluating the first argument as a function. +// +// will check args and try convert input args to func args type. +// +// from text/template/funcs.go#call +func Call(fn reflect.Value, args []reflect.Value, opt *CallOpt) ([]reflect.Value, error) { + fn = indirectInterface(fn) + if !fn.IsValid() { + return nil, fmt.Errorf("call of nil") + } + + typ := fn.Type() + if typ.Kind() != reflect.Func { + return nil, fmt.Errorf("non-function of type %s", typ) + } + + if opt == nil { + opt = &CallOpt{} + } + if opt.TypeChecker != nil { + if err := opt.TypeChecker(typ); err != nil { + return nil, err + } + } + + numIn := typ.NumIn() + var dddType reflect.Type + if typ.IsVariadic() { + if len(args) < numIn-1 { + return nil, fmt.Errorf("wrong number of args: got %d want at least %d", len(args), numIn-1) + } + dddType = typ.In(numIn - 1).Elem() + } else { + if len(args) != numIn { + return nil, fmt.Errorf("wrong number of args: got %d want %d", len(args), numIn) + } + } + + // Convert each arg to the type of the function's arg. + argv := make([]reflect.Value, len(args)) + for i, arg := range args { + arg = indirectInterface(arg) + // Compute the expected type. Clumsy because of variadic. + argType := dddType + if !typ.IsVariadic() || i < numIn-1 { + argType = typ.In(i) + } + + var err error + if argv[i], err = prepareArg(arg, argType); err != nil { + return nil, fmt.Errorf("arg %d: %w", i, err) + } + } + + return SafeCall(fn, argv) +} + +// SafeCall2 runs fun.Call(args), and returns the resulting value and error, if +// any. If the call panics, the panic value is returned as an error. +// +// NOTE: Only support func with 1 or 2 return values: (val) OR (val, err) +// +// from text/template/funcs.go#safeCall +func SafeCall2(fun reflect.Value, args []reflect.Value) (val reflect.Value, err error) { + ret, err := SafeCall(fun, args) + if err != nil { + return reflect.Value{}, err + } + + // func return like: (val, err) + if len(ret) == 2 && !ret[1].IsNil() { + return ret[0], ret[1].Interface().(error) + } + return ret[0], nil +} + +// SafeCall runs fun.Call(args), and returns the resulting values, or an error. +// If the call panics, the panic value is returned as an error. +func SafeCall(fun reflect.Value, args []reflect.Value) (ret []reflect.Value, err error) { + defer func() { + if r := recover(); r != nil { + if e, ok := r.(error); ok { + err = e + } else { + err = fmt.Errorf("%v", r) + } + } + }() + + ret = fun.Call(args) + return +} + +// prepareArg checks if value can be used as an argument of type argType, and +// converts an invalid value to appropriate zero if possible. +func prepareArg(value reflect.Value, argType reflect.Type) (reflect.Value, error) { + if !value.IsValid() { + if !CanBeNil(argType) { + return emptyValue, fmt.Errorf("value is nil; should be of type %s", argType) + } + + value = reflect.Zero(argType) + } + + if value.Type().AssignableTo(argType) { + return value, nil + } + + if IsIntLike(value.Kind()) && IsIntLike(argType.Kind()) && value.Type().ConvertibleTo(argType) { + value = value.Convert(argType) + return value, nil + } + return emptyValue, fmt.Errorf("value has type %s; should be %s", value.Type(), argType) +} + +// indirect returns the item at the end of indirection, and a bool to indicate +// if it's nil. If the returned bool is true, the returned value's kind will be +// either a pointer or interface. +func indirect(v reflect.Value) (rv reflect.Value, isNil bool) { + for ; v.Kind() == reflect.Pointer || v.Kind() == reflect.Interface; v = v.Elem() { + if v.IsNil() { + return v, true + } + } + return v, false +} + +// indirectInterface returns the concrete value in an interface value, +// or else the zero reflect.Value. +// That is, if v represents the interface value x, the result is the same as reflect.ValueOf(x): +// the fact that x was an interface value is forgotten. +func indirectInterface(v reflect.Value) reflect.Value { + if v.Kind() != reflect.Interface { + return v + } + if v.IsNil() { + return reflect.Value{} + } + return v.Elem() +} diff --git a/reflects/func_test.go b/reflects/func_test.go new file mode 100644 index 000000000..b9b9c9e53 --- /dev/null +++ b/reflects/func_test.go @@ -0,0 +1,60 @@ +package reflects_test + +import ( + "errors" + "reflect" + "testing" + + "github.com/gookit/goutil/reflects" + "github.com/gookit/goutil/testutil/assert" +) + +var testFunc1 = func(a, b int) int { + return a + b +} + +var testFunc2 = func(a, b int) (int, error) { + return 0, errors.New("test error") +} + +func TestNewFunc(t *testing.T) { + fx := reflects.NewFunc(reflect.ValueOf(testFunc1)) + assert.Eq(t, "func(int, int) int", fx.String()) + assert.Eq(t, 2, fx.NumIn()) + assert.Eq(t, 1, fx.NumOut()) + + assert.Panics(t, func() { + reflects.NewFunc(nil) + }) + assert.Panics(t, func() { + reflects.NewFunc("invalid") + }) +} + +func TestFuncX_Call(t *testing.T) { + fx := reflects.NewFunc(testFunc1) + + ret, err := fx.Call(1, 2) + assert.NoErr(t, err) + assert.Equal(t, 3, ret[0]) + + // test return error + fx = reflects.NewFunc(testFunc2) + ret, err = fx.Call(1, 2) + assert.NoErr(t, err) + assert.Equal(t, 0, ret[0]) + err = ret[1].(error) + assert.Equal(t, "test error", err.Error()) +} + +func TestFuncX_Call_err(t *testing.T) { + fx := reflects.NewFunc(testFunc1) + + // arg number error + _, err := fx.Call(1, 2, 3) + assert.ErrMsg(t, err, "wrong number of args: got 3 want 2") + + // arg type error + _, err = fx.Call(1, "2") + assert.ErrMsg(t, err, "arg 1: value has type string; should be int") +} diff --git a/reflects/reflects.go b/reflects/reflects.go index 4dcc7cc6c..cd2ea0eb5 100644 --- a/reflects/reflects.go +++ b/reflects/reflects.go @@ -1,20 +1,17 @@ // Package reflects Provide extends reflect util functions. package reflects -import "reflect" +import ( + "fmt" + "reflect" +) -// MakeSliceByElem create a new slice by the element type. -// -// - elType: the type of the element. -// - returns: the new slice. -// -// Usage: -// -// sl := MakeSliceByElem(reflect.TypeOf(1), 10, 20) -// sl.Index(0).SetInt(10) -// -// // Or use reflect.AppendSlice() merge two slice -// // Or use `for` with `reflect.Append()` add elements -func MakeSliceByElem(elTyp reflect.Type, len, cap int) reflect.Value { - return reflect.MakeSlice(reflect.SliceOf(elTyp), len, cap) -} +var emptyValue = reflect.Value{} + +var ( + anyType = reflect.TypeOf((*any)(nil)).Elem() + errorType = reflect.TypeOf((*error)(nil)).Elem() + + fmtStringerType = reflect.TypeOf((*fmt.Stringer)(nil)).Elem() + reflectValueType = reflect.TypeOf((*reflect.Value)(nil)).Elem() +) diff --git a/reflects/util.go b/reflects/util.go index ad441d81a..1485d23bb 100644 --- a/reflects/util.go +++ b/reflects/util.go @@ -78,6 +78,28 @@ func Len(v reflect.Value) int { return -1 } +// IsIntLike reports whether the type is int-like(intX, uintX). +func IsIntLike(typ reflect.Kind) bool { + switch typ { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return true + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return true + } + return false +} + +// CanBeNil reports whether an untyped nil can be assigned to the type. See reflect.Zero. +func CanBeNil(typ reflect.Type) bool { + switch typ.Kind() { + case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice: + return true + case reflect.Struct: + return typ == reflectValueType + } + return false +} + // SliceSubKind get sub-elem kind of the array, slice, variadic-var. alias SliceElemKind() func SliceSubKind(typ reflect.Type) reflect.Kind { return SliceElemKind(typ)