├── Readme.md ├── util_test.go ├── util.go ├── jsoncall.go └── jsoncall_test.go /Readme.md: -------------------------------------------------------------------------------- 1 | # go-jsoncall 2 | 3 | Pretty niche pkg for invoking a function or method via JSON parameters. 4 | 5 | --- 6 | 7 | [![GoDoc](https://godoc.org/github.com/tj/go-jsoncall?status.svg)](https://godoc.org/github.com/tj/go-jsoncall) 8 | ![](https://img.shields.io/badge/license-MIT-blue.svg) 9 | ![](https://img.shields.io/badge/status-stable-green.svg) 10 | 11 | 12 | -------------------------------------------------------------------------------- /util_test.go: -------------------------------------------------------------------------------- 1 | package jsoncall 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/tj/assert" 8 | ) 9 | 10 | // Test type name conversion. 11 | func TestTypeName(t *testing.T) { 12 | cases := []struct { 13 | input interface{} 14 | output string 15 | }{ 16 | {1, "number"}, 17 | {1.5, "number"}, 18 | {"hello", "string"}, 19 | {true, "boolean"}, 20 | {struct{}{}, "object"}, 21 | {map[string]string{}, "object"}, 22 | {[]string{}, "array of strings"}, 23 | {[]bool{}, "array of booleans"}, 24 | {[]int{}, "array of numbers"}, 25 | } 26 | 27 | for _, c := range cases { 28 | t.Run(c.output, func(t *testing.T) { 29 | v := typeName(reflect.TypeOf(c.input)) 30 | assert.Equal(t, c.output, v) 31 | }) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package jsoncall 2 | 3 | import ( 4 | "context" 5 | "reflect" 6 | ) 7 | 8 | // errorInterface is the error interface. 9 | var errorInterface = reflect.TypeOf((*error)(nil)).Elem() 10 | 11 | // contextInterface is the context interface. 12 | var contextInterface = reflect.TypeOf((*context.Context)(nil)).Elem() 13 | 14 | // typeName returns the JSON name of the corresponding Go type. 15 | func typeName(t reflect.Type) string { 16 | switch unrollPointer(t).Kind() { 17 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, 18 | reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, 19 | reflect.Float32, reflect.Float64: 20 | return "number" 21 | case reflect.Slice, reflect.Array: 22 | return "array of " + typeName(t.Elem()) + "s" 23 | case reflect.Bool: 24 | return "boolean" 25 | case reflect.String: 26 | return "string" 27 | case reflect.Map: 28 | return "object" 29 | case reflect.Struct: 30 | return "object" 31 | default: 32 | return "unknown" 33 | } 34 | } 35 | 36 | // unrollPointer unrolls and pointers of a type. 37 | func unrollPointer(t reflect.Type) reflect.Type { 38 | for t.Kind() == reflect.Ptr { 39 | t = t.Elem() 40 | } 41 | return t 42 | } 43 | 44 | // hasContext returns true if the function type has a context argument at the given index. 45 | func hasContext(t reflect.Type, i int) bool { 46 | if t.NumIn() < i+1 { 47 | return false 48 | } 49 | 50 | return isContext(t.In(i)) 51 | } 52 | 53 | // isContext returns true if the given type implements context.Context. 54 | func isContext(t reflect.Type) bool { 55 | return t.Kind() == reflect.Interface && t.Implements(contextInterface) 56 | } 57 | 58 | // isError returns true if the given type implements error. 59 | func isError(t reflect.Type) bool { 60 | return t.Kind() == reflect.Interface && t.Implements(errorInterface) 61 | } 62 | -------------------------------------------------------------------------------- /jsoncall.go: -------------------------------------------------------------------------------- 1 | // Package jsoncall provides utilities for invoking Go functions from JSON. 2 | package jsoncall 3 | 4 | import ( 5 | "context" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "reflect" 10 | "strings" 11 | ) 12 | 13 | // config settings. 14 | type config struct { 15 | contextFunc ContextFunc 16 | arity int 17 | offset int 18 | contextIndex int 19 | } 20 | 21 | // defaultContextFunc is the default context function. 22 | func defaultContextFunc() context.Context { 23 | return context.Background() 24 | } 25 | 26 | // ErrNotFunction is returned when a non-function value is passed. 27 | var ErrNotFunction = errors.New("Must pass a function") 28 | 29 | // ErrTooManyArguments is returned when too many arguments are passed. 30 | var ErrTooManyArguments = errors.New("Too many arguments passed") 31 | 32 | // ErrTooFewArguments is returned when too few arguments are passed. 33 | var ErrTooFewArguments = errors.New("Too few arguments passed") 34 | 35 | // ErrInvalidJSON is returned when the input is malformed. 36 | var ErrInvalidJSON = errors.New("Invalid JSON") 37 | 38 | // errVariadic is returned when a variadic function is used. 39 | var errVariadic = errors.New("Variadic functions are not yet supported") 40 | 41 | // UnmarshalError is an unmarshal error. 42 | type UnmarshalError json.UnmarshalTypeError 43 | 44 | // Error implementation. 45 | func (e UnmarshalError) Error() string { 46 | return fmt.Sprintf("Incorrect type %s, expected %s", e.Value, typeName(e.Type)) 47 | } 48 | 49 | // ContextFunc is used to create a new context. 50 | type ContextFunc func() context.Context 51 | 52 | // Option function. 53 | type Option func(*config) 54 | 55 | // WithContextFunc sets the context function, used to create a new context 56 | // when the function being called expects one. 57 | func WithContextFunc(fn ContextFunc) Option { 58 | return func(v *config) { 59 | v.contextFunc = fn 60 | } 61 | } 62 | 63 | // newConfig returns a new config with options applied. 64 | func newConfig(options []Option) *config { 65 | var c config 66 | c.contextFunc = defaultContextFunc 67 | for _, o := range options { 68 | o(&c) 69 | } 70 | return &c 71 | } 72 | 73 | // Normalize returns a normalized json array string, to be used as parameters. 74 | func Normalize(s string) string { 75 | s = strings.TrimSpace(s) 76 | if len(s) > 0 && s[0] == '[' { 77 | return s 78 | } 79 | return "[" + s + "]" 80 | } 81 | 82 | // CallFunc invokes a function with arguments derived from a json string. 83 | func CallFunc(fn interface{}, args string, options ...Option) ([]reflect.Value, error) { 84 | t := reflect.TypeOf(fn) 85 | 86 | arguments, err := ArgumentsOfFunc(t, args, options...) 87 | if err != nil { 88 | return nil, err 89 | } 90 | 91 | return CallFuncArgs(fn, arguments, options...) 92 | } 93 | 94 | // CallMethod invokes a method on a struct with arguments derived from a json string. 95 | func CallMethod(receiver interface{}, m reflect.Method, args string, options ...Option) ([]reflect.Value, error) { 96 | arguments, err := ArgumentsOfMethod(m, args, options...) 97 | if err != nil { 98 | return nil, err 99 | } 100 | 101 | return CallMethodArgs(receiver, m, arguments, options...) 102 | } 103 | 104 | // CallFuncArgs invokes a function with arguments derived from a json string. 105 | func CallFuncArgs(fn interface{}, args []reflect.Value, options ...Option) (values []reflect.Value, err error) { 106 | // invoke 107 | res := reflect.ValueOf(fn).Call(args) 108 | 109 | // results 110 | for _, v := range res { 111 | if isError(v.Type()) && v.IsValid() && !v.IsNil() { 112 | return nil, v.Interface().(error) 113 | } 114 | values = append(values, v) 115 | } 116 | 117 | return 118 | } 119 | 120 | // CallMethodArgs invokes a method on a struct with arguments derived from a json string. 121 | func CallMethodArgs(receiver interface{}, m reflect.Method, args []reflect.Value, options ...Option) (values []reflect.Value, err error) { 122 | // receiver 123 | r := reflect.ValueOf(receiver) 124 | args = append([]reflect.Value{r}, args...) 125 | 126 | // invoke 127 | res := m.Func.Call(args) 128 | 129 | // results 130 | for _, v := range res { 131 | if isError(v.Type()) && v.IsValid() && !v.IsNil() { 132 | return nil, v.Interface().(error) 133 | } 134 | values = append(values, v) 135 | } 136 | 137 | return 138 | } 139 | 140 | // ArgumentsOfMethod returns arguments for the given method, derived from a json string. 141 | func ArgumentsOfMethod(m reflect.Method, args string, options ...Option) ([]reflect.Value, error) { 142 | c := newConfig(options) 143 | c.arity = m.Type.NumIn() - 1 144 | c.offset = 1 145 | c.contextIndex = 1 146 | return arguments(m.Type, args, c) 147 | } 148 | 149 | // ArgumentsOfFunc returns arguments for the given function, derived from a json string. 150 | func ArgumentsOfFunc(t reflect.Type, args string, options ...Option) ([]reflect.Value, error) { 151 | if t.Kind() != reflect.Func { 152 | return nil, ErrNotFunction 153 | } 154 | c := newConfig(options) 155 | c.arity = t.NumIn() 156 | return arguments(t, args, c) 157 | } 158 | 159 | // arguments implementation. 160 | func arguments(t reflect.Type, s string, c *config) ([]reflect.Value, error) { 161 | var args []reflect.Value 162 | 163 | // ensure it's not variadic 164 | if t.IsVariadic() { 165 | return nil, errVariadic 166 | } 167 | 168 | // inject context 169 | if hasContext(t, c.contextIndex) { 170 | args = append(args, reflect.ValueOf(c.contextFunc())) 171 | c.offset++ 172 | c.arity-- 173 | } 174 | 175 | // parse params 176 | var params []json.RawMessage 177 | 178 | err := json.Unmarshal([]byte(s), ¶ms) 179 | 180 | if _, ok := err.(*json.SyntaxError); ok { 181 | return nil, ErrInvalidJSON 182 | } 183 | 184 | if err != nil { 185 | return nil, err 186 | } 187 | 188 | // too few 189 | if len(params) < c.arity { 190 | return nil, ErrTooFewArguments 191 | } 192 | 193 | // too many 194 | if len(params) > c.arity { 195 | return nil, ErrTooManyArguments 196 | } 197 | 198 | // process the arguments 199 | for i := 0; i < c.arity; i++ { 200 | kind := t.In(c.offset + i) 201 | arg := reflect.New(kind) 202 | value := arg.Interface() 203 | 204 | err := json.Unmarshal(params[i], value) 205 | 206 | if e, ok := err.(*json.UnmarshalTypeError); ok { 207 | return nil, UnmarshalError(*e) 208 | } 209 | 210 | if err != nil { 211 | return nil, err 212 | } 213 | 214 | args = append(args, arg.Elem()) 215 | } 216 | 217 | return args, nil 218 | } 219 | -------------------------------------------------------------------------------- /jsoncall_test.go: -------------------------------------------------------------------------------- 1 | package jsoncall_test 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "math" 7 | "reflect" 8 | "testing" 9 | 10 | "github.com/tj/assert" 11 | jsoncall "github.com/tj/go-jsoncall" 12 | ) 13 | 14 | func abs(v float64) float64 { 15 | return math.Abs(v) 16 | } 17 | 18 | func add(a, b int) int { 19 | return a + b 20 | } 21 | 22 | func sum(nums ...int) (sum int) { 23 | for _, n := range nums { 24 | sum += n 25 | } 26 | return 27 | } 28 | 29 | func avg(nums ...int) int { 30 | return sum(nums...) / len(nums) 31 | } 32 | 33 | type User struct { 34 | Name string `json:"name"` 35 | Email string `json:"email"` 36 | } 37 | 38 | func addUser(u User) error { 39 | return nil 40 | } 41 | 42 | func addUserPointer(u *User) error { 43 | return nil 44 | } 45 | 46 | func addUsers(u []User) error { 47 | return nil 48 | } 49 | 50 | func addUserContext(ctx context.Context, u User) error { 51 | return nil 52 | } 53 | 54 | func addPet(name string) error { 55 | return errors.New("error adding pet") 56 | } 57 | 58 | type mathService struct{} 59 | 60 | func (m *mathService) Sum(ctx context.Context, nums []int) int { 61 | return sum(nums...) 62 | } 63 | 64 | // Test normalization of arguments. 65 | func TestNormalize(t *testing.T) { 66 | assert.Equal(t, `[]`, jsoncall.Normalize(``)) 67 | assert.Equal(t, `[]`, jsoncall.Normalize(`[]`)) 68 | assert.Equal(t, `[5]`, jsoncall.Normalize(`5`)) 69 | assert.Equal(t, `[5]`, jsoncall.Normalize(` 5`)) 70 | assert.Equal(t, `["Hello"]`, jsoncall.Normalize(`"Hello"`)) 71 | assert.Equal(t, `["Hello"]`, jsoncall.Normalize(` "Hello" `)) 72 | assert.Equal(t, `[{ "name": "Tobi" }]`, jsoncall.Normalize(`{ "name": "Tobi" }`)) 73 | assert.Equal(t, `[1, 2, 3]`, jsoncall.Normalize(`[1, 2, 3]`)) 74 | } 75 | 76 | // Test arguments from a function signature. 77 | func TestArgumentsOfFunc(t *testing.T) { 78 | t.Run("should support no results", func(t *testing.T) { 79 | noop := func() {} 80 | vals, err := jsoncall.ArgumentsOfFunc(reflect.TypeOf(noop), `[]`) 81 | assert.NoError(t, err) 82 | assert.Len(t, vals, 0) 83 | }) 84 | 85 | t.Run("should error when the input is invalid json", func(t *testing.T) { 86 | _, err := jsoncall.ArgumentsOfFunc(reflect.TypeOf(add), `[5, hey]`) 87 | assert.EqualError(t, err, `Invalid JSON`) 88 | }) 89 | 90 | t.Run("should error when too few arguments are passed", func(t *testing.T) { 91 | _, err := jsoncall.ArgumentsOfFunc(reflect.TypeOf(add), `[1]`) 92 | assert.EqualError(t, err, `Too few arguments passed`) 93 | }) 94 | 95 | t.Run("should error when too many arguments are passed", func(t *testing.T) { 96 | _, err := jsoncall.ArgumentsOfFunc(reflect.TypeOf(add), `[1, 2, 3]`) 97 | assert.EqualError(t, err, `Too many arguments passed`) 98 | }) 99 | 100 | t.Run("should error when arguments are incorrect types", func(t *testing.T) { 101 | _, err := jsoncall.ArgumentsOfFunc(reflect.TypeOf(add), `[1, "5"]`) 102 | assert.EqualError(t, err, `Incorrect type string, expected number`) 103 | 104 | _, err = jsoncall.ArgumentsOfFunc(reflect.TypeOf(addUser), `["hello"]`) 105 | assert.EqualError(t, err, `Incorrect type string, expected object`) 106 | }) 107 | 108 | t.Run("should support primitives", func(t *testing.T) { 109 | vals, err := jsoncall.ArgumentsOfFunc(reflect.TypeOf(add), `[1, 5]`) 110 | assert.NoError(t, err) 111 | assert.Len(t, vals, 2) 112 | assert.Equal(t, 1, vals[0].Interface().(int)) 113 | assert.Equal(t, 5, vals[1].Interface().(int)) 114 | }) 115 | 116 | t.Run("should support structs", func(t *testing.T) { 117 | vals, err := jsoncall.ArgumentsOfFunc(reflect.TypeOf(addUser), `[{ "name": "Tobi" }]`) 118 | assert.NoError(t, err) 119 | assert.Len(t, vals, 1) 120 | assert.Equal(t, "Tobi", vals[0].Interface().(User).Name) 121 | }) 122 | 123 | t.Run("should support pointers to structs", func(t *testing.T) { 124 | vals, err := jsoncall.ArgumentsOfFunc(reflect.TypeOf(addUserPointer), `[{ "name": "Tobi" }]`) 125 | assert.NoError(t, err) 126 | assert.Len(t, vals, 1) 127 | assert.Equal(t, "Tobi", vals[0].Interface().(*User).Name) 128 | }) 129 | 130 | t.Run("should support null for pointers to structs", func(t *testing.T) { 131 | vals, err := jsoncall.ArgumentsOfFunc(reflect.TypeOf(addUserPointer), `[null]`) 132 | assert.NoError(t, err) 133 | assert.Len(t, vals, 1) 134 | assert.Empty(t, vals[0].Interface()) 135 | }) 136 | 137 | t.Run("should support context as the first argument", func(t *testing.T) { 138 | vals, err := jsoncall.ArgumentsOfFunc(reflect.TypeOf(addUserContext), `[{ "name": "Tobi" }]`) 139 | assert.NoError(t, err) 140 | assert.Len(t, vals, 2) 141 | assert.Implements(t, (*context.Context)(nil), vals[0].Interface(), "should have a context") 142 | assert.Equal(t, "Tobi", vals[1].Interface().(User).Name) 143 | }) 144 | 145 | t.Run("should support custom contexts via WithContextFunc", func(t *testing.T) { 146 | var called bool 147 | _, err := jsoncall.ArgumentsOfFunc(reflect.TypeOf(addUserContext), `[{ "name": "Tobi" }]`, jsoncall.WithContextFunc(func() context.Context { 148 | called = true 149 | return context.TODO() 150 | })) 151 | assert.NoError(t, err) 152 | assert.True(t, called, "should call the function") 153 | }) 154 | 155 | t.Run("should support slices of structs", func(t *testing.T) { 156 | vals, err := jsoncall.ArgumentsOfFunc(reflect.TypeOf(addUsers), `[[{ "name": "Tobi" }, { "name": "Loki" }]]`) 157 | assert.NoError(t, err) 158 | assert.Len(t, vals, 1) 159 | assert.Equal(t, "Tobi", vals[0].Interface().([]User)[0].Name) 160 | }) 161 | 162 | t.Run("should error on variadic functions", func(t *testing.T) { 163 | // TODO: support variadic functions 164 | _, err := jsoncall.ArgumentsOfFunc(reflect.TypeOf(sum), `[1, 2, 3, 4]`) 165 | assert.EqualError(t, err, `Variadic functions are not yet supported`) 166 | }) 167 | } 168 | 169 | // Test arguments from a method signature. 170 | func TestArgumentsOfMethod(t *testing.T) { 171 | s := &mathService{} 172 | 173 | m, ok := reflect.TypeOf(s).MethodByName("Sum") 174 | assert.True(t, ok) 175 | 176 | vals, err := jsoncall.ArgumentsOfMethod(m, `[[1,2,3,4]]`) 177 | assert.NoError(t, err) 178 | assert.Implements(t, (*context.Context)(nil), vals[0].Interface(), "should have a context") 179 | assert.Equal(t, []int{1, 2, 3, 4}, vals[1].Interface()) 180 | } 181 | 182 | // Test calling of functions. 183 | func TestCallFunc(t *testing.T) { 184 | t.Run("should support returning a value", func(t *testing.T) { 185 | add := func(a, b int) int { return a + b } 186 | v, err := jsoncall.CallFunc(add, `[1,2]`) 187 | assert.NoError(t, err) 188 | assert.Len(t, v, 1) 189 | assert.Equal(t, 3, v[0].Interface()) 190 | }) 191 | 192 | t.Run("should support returning errors", func(t *testing.T) { 193 | add := func(a, b int) error { return errors.New("boom") } 194 | _, err := jsoncall.CallFunc(add, `[1,2]`) 195 | assert.EqualError(t, err, `boom`) 196 | }) 197 | 198 | t.Run("should support returning values and errors", func(t *testing.T) { 199 | add := func(a, b int) (int, error) { return a + b, nil } 200 | v, err := jsoncall.CallFunc(add, `[1,2]`) 201 | assert.NoError(t, err) 202 | assert.Equal(t, 3, v[0].Interface()) 203 | }) 204 | 205 | t.Run("should support returning multiple values", func(t *testing.T) { 206 | minmax := func(a, b int) (min, max int) { return a, b } 207 | v, err := jsoncall.CallFunc(minmax, `[1,2]`) 208 | assert.NoError(t, err) 209 | assert.Equal(t, 1, v[0].Interface()) 210 | assert.Equal(t, 2, v[1].Interface()) 211 | }) 212 | 213 | t.Run("should support returning multiple values and errors", func(t *testing.T) { 214 | minmax := func(a, b int) (min, max int, err error) { return a, b, nil } 215 | v, err := jsoncall.CallFunc(minmax, `[1,2]`) 216 | assert.NoError(t, err) 217 | assert.Equal(t, 1, v[0].Interface()) 218 | assert.Equal(t, 2, v[1].Interface()) 219 | }) 220 | 221 | t.Run("should support returning errors", func(t *testing.T) { 222 | _, err := jsoncall.CallFunc(addPet, `["Tobi"]`) 223 | assert.EqualError(t, err, `error adding pet`) 224 | }) 225 | } 226 | 227 | // Test calling of methods. 228 | func TestCallMethod(t *testing.T) { 229 | t.Run("should support returning a value", func(t *testing.T) { 230 | s := &mathService{} 231 | m, _ := reflect.TypeOf(s).MethodByName("Sum") 232 | v, err := jsoncall.CallMethod(s, m, `[[1,2]]`) 233 | assert.NoError(t, err) 234 | assert.Len(t, v, 1) 235 | assert.Equal(t, 3, v[0].Interface()) 236 | }) 237 | } 238 | 239 | // Benchmark argument reflection. 240 | func BenchmarkArguments(b *testing.B) { 241 | b.SetBytes(1) 242 | t := reflect.TypeOf(addUsers) 243 | for i := 0; i < b.N; i++ { 244 | jsoncall.ArgumentsOfFunc(t, `[[{ "name": "Tobi" }, { "name": "Loki" }]]`) 245 | } 246 | } 247 | 248 | // Benchmark function calling. 249 | func BenchmarkCallFunc(b *testing.B) { 250 | b.SetBytes(1) 251 | for i := 0; i < b.N; i++ { 252 | jsoncall.CallFunc(addUser, `[[{ "name": "Tobi" }, { "name": "Loki" }]]`) 253 | } 254 | } 255 | --------------------------------------------------------------------------------