diff --git a/internal/dynfunc/handler.go b/internal/dynfunc/handler.go index 4c544bc..634cfc6 100644 --- a/internal/dynfunc/handler.go +++ b/internal/dynfunc/handler.go @@ -11,7 +11,7 @@ import ( // Handler represents a dynamic api handler type Handler struct { - spec *spec + spec *signature fn interface{} // whether fn uses api.Ctx as 1st argument hasContext bool @@ -31,7 +31,7 @@ type Handler struct { // - it there is no output, `outputStruct` must be omitted func Build(fn interface{}, service config.Service) (*Handler, error) { h := &Handler{ - spec: makeSpec(service), + spec: signatureFromService(service), fn: fn, } @@ -82,8 +82,8 @@ func (h *Handler) Handle(ctx api.Ctx, data map[string]interface{}) (map[string]i } // get value from @data - value, inData := data[name] - if !inData { + value, provided := data[name] + if !provided { continue } @@ -94,7 +94,7 @@ func (h *Handler) Handle(ctx api.Ctx, data map[string]interface{}) (map[string]i var ptrType = field.Type().Elem() if !refvalue.Type().ConvertibleTo(ptrType) { - log.Printf("Cannot convert %v into %v", refvalue.Type(), ptrType) + log.Printf("Cannot convert %v into *%v", refvalue.Type(), ptrType) return nil, api.ErrUncallableService } @@ -104,12 +104,13 @@ func (h *Handler) Handle(ctx api.Ctx, data map[string]interface{}) (map[string]i field.Set(ptr) continue } + if !reflect.ValueOf(value).Type().ConvertibleTo(field.Type()) { log.Printf("Cannot convert %v into %v", reflect.ValueOf(value).Type(), field.Type()) return nil, api.ErrUncallableService } - field.Set(reflect.ValueOf(value).Convert(field.Type())) + field.Set(refvalue.Convert(field.Type())) } callArgs = append(callArgs, callStruct) } diff --git a/internal/dynfunc/handler_test.go b/internal/dynfunc/handler_test.go new file mode 100644 index 0000000..a457f1e --- /dev/null +++ b/internal/dynfunc/handler_test.go @@ -0,0 +1,173 @@ +package dynfunc + +import ( + "fmt" + "reflect" + "testing" + + "git.xdrm.io/go/aicra/api" +) + +type testsignature signature + +// builds a mock service with provided arguments as Input and matched as Output +func (s *testsignature) withArgs(dtypes ...reflect.Type) *testsignature { + if s.Input == nil { + s.Input = make(map[string]reflect.Type) + } + if s.Output == nil { + s.Output = make(map[string]reflect.Type) + } + + for i, dtype := range dtypes { + name := fmt.Sprintf("P%d", i+1) + s.Input[name] = dtype + if dtype.Kind() == reflect.Ptr { + s.Output[name] = dtype.Elem() + } else { + s.Output[name] = dtype + } + } + return s +} + +func TestInput(t *testing.T) { + + type intstruct struct { + P1 int + } + type intptrstruct struct { + P1 *int + } + + tcases := []struct { + Name string + Spec *testsignature + HasContext bool + Fn interface{} + Input []interface{} + ExpectedOutput []interface{} + ExpectedErr api.Err + }{ + { + Name: "none required none provided", + Spec: (&testsignature{}).withArgs(), + Fn: func() (*struct{}, api.Err) { return nil, api.ErrSuccess }, + HasContext: false, + Input: []interface{}{}, + ExpectedOutput: []interface{}{}, + ExpectedErr: api.ErrSuccess, + }, + { + Name: "int proxy (0)", + Spec: (&testsignature{}).withArgs(reflect.TypeOf(int(0))), + Fn: func(in intstruct) (*intstruct, api.Err) { + return &intstruct{P1: in.P1}, api.ErrSuccess + }, + HasContext: false, + Input: []interface{}{int(0)}, + ExpectedOutput: []interface{}{int(0)}, + ExpectedErr: api.ErrSuccess, + }, + { + Name: "int proxy (11)", + Spec: (&testsignature{}).withArgs(reflect.TypeOf(int(0))), + Fn: func(in intstruct) (*intstruct, api.Err) { + return &intstruct{P1: in.P1}, api.ErrSuccess + }, + HasContext: false, + Input: []interface{}{int(11)}, + ExpectedOutput: []interface{}{int(11)}, + ExpectedErr: api.ErrSuccess, + }, + { + Name: "*int proxy (nil)", + Spec: (&testsignature{}).withArgs(reflect.TypeOf(new(int))), + Fn: func(in intptrstruct) (*intptrstruct, api.Err) { + return &intptrstruct{P1: in.P1}, api.ErrSuccess + }, + HasContext: false, + Input: []interface{}{}, + ExpectedOutput: []interface{}{nil}, + ExpectedErr: api.ErrSuccess, + }, + { + Name: "*int proxy (28)", + Spec: (&testsignature{}).withArgs(reflect.TypeOf(new(int))), + Fn: func(in intptrstruct) (*intstruct, api.Err) { + return &intstruct{P1: *in.P1}, api.ErrSuccess + }, + HasContext: false, + Input: []interface{}{28}, + ExpectedOutput: []interface{}{28}, + ExpectedErr: api.ErrSuccess, + }, + { + Name: "*int proxy (13)", + Spec: (&testsignature{}).withArgs(reflect.TypeOf(new(int))), + Fn: func(in intptrstruct) (*intstruct, api.Err) { + return &intstruct{P1: *in.P1}, api.ErrSuccess + }, + HasContext: false, + Input: []interface{}{13}, + ExpectedOutput: []interface{}{13}, + ExpectedErr: api.ErrSuccess, + }, + } + + for _, tcase := range tcases { + t.Run(tcase.Name, func(t *testing.T) { + t.Parallel() + + var dataIndex = 0 + if tcase.HasContext { + dataIndex = 1 + } + + var handler = &Handler{ + spec: &signature{Input: tcase.Spec.Input, Output: tcase.Spec.Output}, + fn: tcase.Fn, + dataIndex: dataIndex, + hasContext: tcase.HasContext, + } + + // build input + input := make(map[string]interface{}) + for i, val := range tcase.Input { + var key = fmt.Sprintf("P%d", i+1) + input[key] = val + } + + var output, err = handler.Handle(api.Ctx{}, input) + if err != tcase.ExpectedErr { + t.Fatalf("expected api error <%v> got <%v>", tcase.ExpectedErr, err) + } + + // check output + for i, expected := range tcase.ExpectedOutput { + var ( + key = fmt.Sprintf("P%d", i+1) + val, exists = output[key] + ) + if !exists { + t.Fatalf("missing output[%s]", key) + } + if expected != val { + var ( + expectedt = reflect.ValueOf(expected) + valt = reflect.ValueOf(val) + expectedNil = !expectedt.IsValid() || expectedt.Kind() == reflect.Ptr && expectedt.IsNil() + valNil = !valt.IsValid() || valt.Kind() == reflect.Ptr && valt.IsNil() + ) + // ignore both nil + if valNil && expectedNil { + continue + } + t.Fatalf("expected output[%s] to equal %T <%v> got %T <%v>", key, expected, expected, val, val) + } + } + + }) + } + +} diff --git a/internal/dynfunc/spec.go b/internal/dynfunc/signature.go similarity index 90% rename from internal/dynfunc/spec.go rename to internal/dynfunc/signature.go index 8f43ccd..2ee32ae 100644 --- a/internal/dynfunc/spec.go +++ b/internal/dynfunc/signature.go @@ -9,14 +9,15 @@ import ( "git.xdrm.io/go/aicra/internal/config" ) -type spec struct { +// signature represents input/output arguments for a dynamic function +type signature struct { Input map[string]reflect.Type Output map[string]reflect.Type } // builds a spec from the configuration service -func makeSpec(service config.Service) *spec { - s := &spec{ +func signatureFromService(service config.Service) *signature { + s := &signature{ Input: make(map[string]reflect.Type), Output: make(map[string]reflect.Type), } @@ -44,7 +45,7 @@ func makeSpec(service config.Service) *spec { } // checks for HandlerFn input arguments -func (s *spec) checkInput(impl reflect.Type, index int) error { +func (s *signature) checkInput(impl reflect.Type, index int) error { var requiredInput, structIndex = index, index if len(s.Input) > 0 { // arguments struct requiredInput++ @@ -91,7 +92,7 @@ func (s *spec) checkInput(impl reflect.Type, index int) error { } // checks for HandlerFn output arguments -func (s spec) checkOutput(impl reflect.Type) error { +func (s signature) checkOutput(impl reflect.Type) error { if impl.NumOut() < 1 { return errMissingHandlerOutput } diff --git a/internal/dynfunc/spec_test.go b/internal/dynfunc/signature_test.go similarity index 99% rename from internal/dynfunc/spec_test.go rename to internal/dynfunc/signature_test.go index 110db15..8860a92 100644 --- a/internal/dynfunc/spec_test.go +++ b/internal/dynfunc/signature_test.go @@ -225,7 +225,7 @@ func TestInputCheck(t *testing.T) { t.Parallel() // mock spec - s := spec{ + s := signature{ Input: tcase.Input, Output: nil, } @@ -371,7 +371,7 @@ func TestOutputCheck(t *testing.T) { t.Parallel() // mock spec - s := spec{ + s := signature{ Input: nil, Output: tcase.Output, }