diff --git a/plugin/shared/call.go b/plugin/shared/call.go index b0c58ee1..49cafad4 100644 --- a/plugin/shared/call.go +++ b/plugin/shared/call.go @@ -3,24 +3,97 @@ package shared import ( "fmt" "reflect" + + "github.com/rs/zerolog/log" ) // CallFunc calls function with arguments // it is used when calling go plugin or builtin functions func CallFunc(fn reflect.Value, args ...interface{}) (interface{}, error) { fnArgsNum := fn.Type().NumIn() - if fnArgsNum > 0 && fn.Type().In(fnArgsNum-1).Kind() == reflect.Slice { - // last argument is slice, do not check arguments number - // e.g. ...interface{} - // e.g. a, b string, c ...interface{} - } else if fnArgsNum != len(args) { - // function arguments not match - return nil, fmt.Errorf("function arguments number not match, expect %d, got %d", fnArgsNum, len(args)) - } - // arguments do not have slice, and arguments number matched - argumentsValue := make([]reflect.Value, len(args)) - for index, argument := range args { + // function expect 0 argument + if fnArgsNum == 0 { + if len(args) > 0 { + return nil, fmt.Errorf("function expect 0 argument, but got %d", len(args)) + } + return call(fn, nil) + } + + // fnArgsNum > 0: function expect more than 0 arguments + // function last argument is not slice + if fn.Type().In(fnArgsNum-1).Kind() != reflect.Slice { + // function arguments should match exactly + if fnArgsNum != len(args) { + return nil, fmt.Errorf("function expect %d arguments, but got %d", fnArgsNum, len(args)) + } + // prepare arguments + argumentsValue, err := convertFirstNArgs(fn, fnArgsNum, args...) + if err != nil { + return nil, err + } + return call(fn, argumentsValue) + } + + // function last argument is slice + // e.g. + // ...interface{} + // a, b string, c ...interface{} + // []int + // a, b string, c []int + + // arguments number should >= fnArgsNum-1 + if len(args) < fnArgsNum-1 { + return nil, fmt.Errorf("function expect at least %d arguments, but got %d", fnArgsNum-1, len(args)) + } + + // prepare first n-1 arguments + argumentsValue, err := convertFirstNArgs(fn, fnArgsNum-1, args...) + if err != nil { + return nil, err + } + + // prepare last slice argument + fnLastSliceElemType := fn.Type().In(fnArgsNum - 1).Elem() // slice element type + for i := fnArgsNum - 1; i < len(args); i++ { // loop last len(args)-fnArgsNum+1 arguments + argValue := reflect.ValueOf(args[i]) + argValueType := reflect.TypeOf(args[i]) + + // check if slice element type match + if fnLastSliceElemType == argValueType { + argumentsValue = append(argumentsValue, argValue) + continue + } + + // last argument is also slice, e.g. []int + if argValueType.Kind() == reflect.Slice { + if argValueType.Elem() != fnLastSliceElemType { + err := fmt.Errorf("function argument %d's slice element type is not match, expect %v, actual %v", + i, fnLastSliceElemType, argValueType) + return nil, err + } + argumentsValue = append(argumentsValue, argValue) + continue + } + + // type not match, check if convertible + if !argValueType.ConvertibleTo(fnLastSliceElemType) { + // function argument type not match and not convertible + err := fmt.Errorf("function argument %d's type is neither match nor convertible, expect %v, actual %v", + i, fnLastSliceElemType, argValueType) + return nil, err + } + // convert argument to expect type + argumentsValue = append(argumentsValue, argValue.Convert(fnLastSliceElemType)) + } + + return call(fn, argumentsValue) +} + +func convertFirstNArgs(fn reflect.Value, n int, args ...interface{}) ([]reflect.Value, error) { + argumentsValue := make([]reflect.Value, n) + for index := 0; index < n; index++ { + argument := args[index] if argument == nil { argumentsValue[index] = reflect.Zero(fn.Type().In(index)) continue @@ -41,13 +114,17 @@ func CallFunc(fn reflect.Value, args ...interface{}) (interface{}, error) { // function argument type not match and not convertible err := fmt.Errorf("function argument %d's type is neither match nor convertible, expect %v, actual %v", index, expectArgumentType, actualArgumentType) + log.Error().Err(err).Msg("convert arguments failed") return nil, err } // convert argument to expect type argumentsValue[index] = argumentValue.Convert(expectArgumentType) } + return argumentsValue, nil +} - resultValues := fn.Call(argumentsValue) +func call(fn reflect.Value, args []reflect.Value) (interface{}, error) { + resultValues := fn.Call(args) if resultValues == nil { // no returns return nil, nil @@ -69,7 +146,7 @@ func CallFunc(fn reflect.Value, args ...interface{}) (interface{}, error) { } } else { // return more than 2 arguments, unexpected - err := fmt.Errorf("function should return at most 2 arguments") + err := fmt.Errorf("function should return at most 2 values") return nil, err } } diff --git a/plugin/shared/call_test.go b/plugin/shared/call_test.go index 9b19933a..4f45fa70 100644 --- a/plugin/shared/call_test.go +++ b/plugin/shared/call_test.go @@ -9,13 +9,14 @@ import ( "github.com/stretchr/testify/assert" ) -func TestCallFunc(t *testing.T) { - type data struct { - f interface{} - args []interface{} - expVal interface{} - expErr error - } +type data struct { + f interface{} + args []interface{} + expVal interface{} + expErr error +} + +func TestCallFuncBasic(t *testing.T) { params := []data{ // zero argument, zero return {f: func() {}, args: []interface{}{}, expVal: nil, expErr: nil}, @@ -28,10 +29,16 @@ func TestCallFunc(t *testing.T) { // zero argument, return one value and error {f: func() (int, error) { return 1, errors.New("xxx") }, args: []interface{}{}, expVal: 1, expErr: errors.New("xxx")}, {f: func() (interface{}, error) { return 1.23, errors.New("xxx") }, args: []interface{}{}, expVal: 1.23, expErr: errors.New("xxx")}, + + // one argument, zero return + {f: func(n int) {}, args: []interface{}{1}, expVal: nil, expErr: nil}, // one argument, return one value {f: func(n int) int { return n * n }, args: []interface{}{2}, expVal: 4}, {f: func(c string) string { return c + c }, args: []interface{}{"p"}, expVal: "pp"}, {f: func(arg interface{}) interface{} { return fmt.Sprintf("%v", arg) }, args: []interface{}{1.23}, expVal: "1.23"}, + // one argument, return one value and error + {f: func(arg interface{}) (interface{}, error) { return 1.23, errors.New("xxx") }, args: []interface{}{"a"}, expVal: 1.23, expErr: errors.New("xxx")}, + // two arguments in same type {f: func(a, b int) int { return a * b }, args: []interface{}{2, 3}, expVal: 6}, // two arguments in different type @@ -46,6 +53,123 @@ func TestCallFunc(t *testing.T) { args: []interface{}{3, "p"}, expVal: "ppp", }, + + // variable arguments list: ...int, ...interface{} + { + f: func(n ...int) int { + var sum int + for _, arg := range n { + sum += arg + } + return sum + }, + args: []interface{}{1, 2, 3}, + expVal: 6, + }, + { + f: func(args ...interface{}) (interface{}, error) { + var result string + for _, arg := range args { + result += fmt.Sprintf("%v", arg) + } + return result, nil + }, + args: []interface{}{1, 2.3, "4.5", "p"}, + expVal: "12.34.5p", + }, + { + f: func(a, b int8, n ...int) int { + var sum int + for _, arg := range n { + sum += arg + } + sum += int(a) + int(b) + return sum + }, + args: []interface{}{1, 2, 3, 4.5}, + expVal: 10, + }, + { + f: func(a, b int8, n ...int) int { + sum := int(a) + int(b) + for _, arg := range n { + sum += arg + } + return sum + }, + args: []interface{}{1, 2}, + expVal: 3, + }, + + { + f: func(a []int, n ...int) int { + var sum int + for _, arg := range a { + sum += arg + } + for _, arg := range n { + sum += arg + } + return sum + }, + args: []interface{}{[]int{1, 2}, 3, 4}, + expVal: 10, + }, + } + + for _, p := range params { + fn := reflect.ValueOf(p.f) + val, err := CallFunc(fn, p.args...) + if !assert.Equal(t, p.expErr, err) { + t.Fatal(err) + } + if !assert.Equal(t, p.expVal, val) { + t.Fatal() + } + } + +} + +func TestCallFuncComplex(t *testing.T) { + params := []data{ + // arguments include slice + { + f: func(a int, n []int, b int) int { + sum := a + for _, arg := range n { + sum += arg + } + sum += b + return sum + }, + args: []interface{}{1, []int{2, 3}, 4}, + expVal: 10, + }, + // last argument is slice + { + f: func(n []int) int { + var sum int + for _, arg := range n { + sum += arg + } + return sum + }, + args: []interface{}{[]int{1, 2, 3}}, + expVal: 6, + }, + { + f: func(a, b int, n []int) int { + sum := a + b + for _, arg := range n { + sum += arg + } + return sum + }, + args: []interface{}{1, 2, []int{3, 4}}, + expVal: 10, + }, + + // FIXME: return more than 2 values } for _, p := range params {