From 0a55c2ee134f558019c67f8ecc38927a7285222c Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Sun, 18 Apr 2021 19:25:31 +0200 Subject: [PATCH] feat: add optional api.Ctx first argument to handler checker --- internal/dynfunc/handler.go | 19 ++-- internal/dynfunc/spec.go | 47 +++++----- internal/dynfunc/spec_test.go | 158 ++++++++++++++++++++++------------ 3 files changed, 144 insertions(+), 80 deletions(-) diff --git a/internal/dynfunc/handler.go b/internal/dynfunc/handler.go index 4e36b71..f7566c8 100644 --- a/internal/dynfunc/handler.go +++ b/internal/dynfunc/handler.go @@ -11,8 +11,10 @@ import ( // Handler represents a dynamic api handler type Handler struct { - spec spec + spec *spec fn interface{} + // whether fn uses api.Ctx as 1st argument + hasContext bool } // Build a handler from a service configuration and a dynamic function @@ -30,16 +32,23 @@ func Build(fn interface{}, service config.Service) (*Handler, error) { fn: fn, } - fnv := reflect.ValueOf(fn) + impl := reflect.TypeOf(fn) - if fnv.Type().Kind() != reflect.Func { + if impl.Kind() != reflect.Func { return nil, errHandlerNotFunc } - if err := h.spec.checkInput(fnv); err != nil { + h.hasContext = impl.NumIn() >= 1 && reflect.TypeOf(api.Ctx{}).AssignableTo(impl.In(0)) + + inputIndex := 0 + if h.hasContext { + inputIndex = 1 + } + + if err := h.spec.checkInput(impl, inputIndex); err != nil { return nil, fmt.Errorf("input: %w", err) } - if err := h.spec.checkOutput(fnv); err != nil { + if err := h.spec.checkOutput(impl); err != nil { return nil, fmt.Errorf("output: %w", err) } diff --git a/internal/dynfunc/spec.go b/internal/dynfunc/spec.go index 571c9ae..9f487f4 100644 --- a/internal/dynfunc/spec.go +++ b/internal/dynfunc/spec.go @@ -12,11 +12,13 @@ import ( type spec struct { Input map[string]reflect.Type Output map[string]reflect.Type + // HasContext defines whether the given handler has api.Ctx as first argument + HasContext bool } // builds a spec from the configuration service -func makeSpec(service config.Service) spec { - spec := spec{ +func makeSpec(service config.Service) *spec { + s := &spec{ Input: make(map[string]reflect.Type), Output: make(map[string]reflect.Type), } @@ -27,40 +29,46 @@ func makeSpec(service config.Service) spec { } // make a pointer if optional if param.Optional { - spec.Input[param.Rename] = reflect.PtrTo(param.ExtractType) + s.Input[param.Rename] = reflect.PtrTo(param.ExtractType) continue } - spec.Input[param.Rename] = param.ExtractType + s.Input[param.Rename] = param.ExtractType } for _, param := range service.Output { if len(param.Rename) < 1 { continue } - spec.Output[param.Rename] = param.ExtractType + s.Output[param.Rename] = param.ExtractType } - return spec + return s } // checks for HandlerFn input arguments -func (s spec) checkInput(fnv reflect.Value) error { - fnt := fnv.Type() +func (s *spec) checkInput(impl reflect.Type, index int) error { + var requiredInput, structIndex = index, index + if len(s.Input) > 0 { // arguments struct + requiredInput++ + } - // no input -> ok + // missing arguments + if impl.NumIn() > requiredInput { + return errUnexpectedInput + } + + // none required if len(s.Input) == 0 { - if fnt.NumIn() > 0 { - return errUnexpectedInput - } return nil } - if fnt.NumIn() != 1 { + // too much arguments + if impl.NumIn() != requiredInput { return errMissingHandlerArgumentParam } // arg must be a struct - structArg := fnt.In(0) + structArg := impl.In(structIndex) if structArg.Kind() != reflect.Struct { return errMissingParamArgument } @@ -85,14 +93,13 @@ func (s spec) checkInput(fnv reflect.Value) error { } // checks for HandlerFn output arguments -func (s spec) checkOutput(fnv reflect.Value) error { - fnt := fnv.Type() - if fnt.NumOut() < 1 { +func (s spec) checkOutput(impl reflect.Type) error { + if impl.NumOut() < 1 { return errMissingHandlerOutput } // last output must be api.Err - errOutput := fnt.Out(fnt.NumOut() - 1) + errOutput := impl.Out(impl.NumOut() - 1) if !errOutput.AssignableTo(reflect.TypeOf(api.ErrUnknown)) { return errMissingHandlerErrorOutput } @@ -102,12 +109,12 @@ func (s spec) checkOutput(fnv reflect.Value) error { return nil } - if fnt.NumOut() != 2 { + if impl.NumOut() != 2 { return errMissingParamOutput } // fail if first output is not a pointer to struct - structOutputPtr := fnt.Out(0) + structOutputPtr := impl.Out(0) if structOutputPtr.Kind() != reflect.Ptr { return errMissingParamOutput } diff --git a/internal/dynfunc/spec_test.go b/internal/dynfunc/spec_test.go index b361893..110db15 100644 --- a/internal/dynfunc/spec_test.go +++ b/internal/dynfunc/spec_test.go @@ -14,24 +14,28 @@ func TestInputCheck(t *testing.T) { Name string Input map[string]reflect.Type Fn interface{} + FnCtx interface{} Err error }{ { Name: "no input 0 given", Input: map[string]reflect.Type{}, Fn: func() {}, + FnCtx: func(api.Ctx) {}, Err: nil, }, { Name: "no input 1 given", Input: map[string]reflect.Type{}, Fn: func(int) {}, + FnCtx: func(api.Ctx, int) {}, Err: errUnexpectedInput, }, { Name: "no input 2 given", Input: map[string]reflect.Type{}, Fn: func(int, string) {}, + FnCtx: func(api.Ctx, int, string) {}, Err: errUnexpectedInput, }, { @@ -39,187 +43,229 @@ func TestInputCheck(t *testing.T) { Input: map[string]reflect.Type{ "Test1": reflect.TypeOf(int(0)), }, - Fn: func() {}, - Err: errMissingHandlerArgumentParam, + Fn: func() {}, + FnCtx: func(api.Ctx) {}, + Err: errMissingHandlerArgumentParam, }, { Name: "1 input non-struct given", Input: map[string]reflect.Type{ "Test1": reflect.TypeOf(int(0)), }, - Fn: func(int) {}, - Err: errMissingParamArgument, + Fn: func(int) {}, + FnCtx: func(api.Ctx, int) {}, + Err: errMissingParamArgument, }, { Name: "unexported input", Input: map[string]reflect.Type{ "test1": reflect.TypeOf(int(0)), }, - Fn: func(struct{}) {}, - Err: errUnexportedName, + Fn: func(struct{}) {}, + FnCtx: func(api.Ctx, struct{}) {}, + Err: errUnexportedName, }, { Name: "1 input empty struct given", Input: map[string]reflect.Type{ "Test1": reflect.TypeOf(int(0)), }, - Fn: func(struct{}) {}, - Err: errMissingParamFromConfig, + Fn: func(struct{}) {}, + FnCtx: func(api.Ctx, struct{}) {}, + Err: errMissingParamFromConfig, }, { Name: "1 input invalid given", Input: map[string]reflect.Type{ "Test1": reflect.TypeOf(int(0)), }, - Fn: func(struct{ Test1 string }) {}, - Err: errWrongParamTypeFromConfig, + Fn: func(struct{ Test1 string }) {}, + FnCtx: func(api.Ctx, struct{ Test1 string }) {}, + Err: errWrongParamTypeFromConfig, }, { Name: "1 input valid given", Input: map[string]reflect.Type{ "Test1": reflect.TypeOf(int(0)), }, - Fn: func(struct{ Test1 int }) {}, - Err: nil, + Fn: func(struct{ Test1 int }) {}, + FnCtx: func(api.Ctx, struct{ Test1 int }) {}, + Err: nil, }, { Name: "1 input ptr empty struct given", Input: map[string]reflect.Type{ "Test1": reflect.TypeOf(new(int)), }, - Fn: func(struct{}) {}, - Err: errMissingParamFromConfig, + Fn: func(struct{}) {}, + FnCtx: func(api.Ctx, struct{}) {}, + Err: errMissingParamFromConfig, }, { Name: "1 input ptr invalid given", Input: map[string]reflect.Type{ "Test1": reflect.TypeOf(new(int)), }, - Fn: func(struct{ Test1 string }) {}, - Err: errWrongParamTypeFromConfig, + Fn: func(struct{ Test1 string }) {}, + FnCtx: func(api.Ctx, struct{ Test1 string }) {}, + Err: errWrongParamTypeFromConfig, }, { Name: "1 input ptr invalid ptr type given", Input: map[string]reflect.Type{ "Test1": reflect.TypeOf(new(int)), }, - Fn: func(struct{ Test1 *string }) {}, - Err: errWrongParamTypeFromConfig, + Fn: func(struct{ Test1 *string }) {}, + FnCtx: func(api.Ctx, struct{ Test1 *string }) {}, + Err: errWrongParamTypeFromConfig, }, { Name: "1 input ptr valid given", Input: map[string]reflect.Type{ "Test1": reflect.TypeOf(new(int)), }, - Fn: func(struct{ Test1 *int }) {}, - Err: nil, + Fn: func(struct{ Test1 *int }) {}, + FnCtx: func(api.Ctx, struct{ Test1 *int }) {}, + Err: nil, }, { Name: "1 valid string", Input: map[string]reflect.Type{ "Test1": reflect.TypeOf(string("")), }, - Fn: func(struct{ Test1 string }) {}, - Err: nil, + Fn: func(struct{ Test1 string }) {}, + FnCtx: func(api.Ctx, struct{ Test1 string }) {}, + Err: nil, }, { Name: "1 valid uint", Input: map[string]reflect.Type{ "Test1": reflect.TypeOf(uint(0)), }, - Fn: func(struct{ Test1 uint }) {}, - Err: nil, + Fn: func(struct{ Test1 uint }) {}, + FnCtx: func(api.Ctx, struct{ Test1 uint }) {}, + Err: nil, }, { Name: "1 valid float64", Input: map[string]reflect.Type{ "Test1": reflect.TypeOf(float64(0)), }, - Fn: func(struct{ Test1 float64 }) {}, - Err: nil, + Fn: func(struct{ Test1 float64 }) {}, + FnCtx: func(api.Ctx, struct{ Test1 float64 }) {}, + Err: nil, }, { Name: "1 valid []byte", Input: map[string]reflect.Type{ "Test1": reflect.TypeOf([]byte("")), }, - Fn: func(struct{ Test1 []byte }) {}, - Err: nil, + Fn: func(struct{ Test1 []byte }) {}, + FnCtx: func(api.Ctx, struct{ Test1 []byte }) {}, + Err: nil, }, { Name: "1 valid []rune", Input: map[string]reflect.Type{ "Test1": reflect.TypeOf([]rune("")), }, - Fn: func(struct{ Test1 []rune }) {}, - Err: nil, + Fn: func(struct{ Test1 []rune }) {}, + FnCtx: func(api.Ctx, struct{ Test1 []rune }) {}, + Err: nil, }, { Name: "1 valid *string", Input: map[string]reflect.Type{ "Test1": reflect.TypeOf(new(string)), }, - Fn: func(struct{ Test1 *string }) {}, - Err: nil, + Fn: func(struct{ Test1 *string }) {}, + FnCtx: func(api.Ctx, struct{ Test1 *string }) {}, + Err: nil, }, { Name: "1 valid *uint", Input: map[string]reflect.Type{ "Test1": reflect.TypeOf(new(uint)), }, - Fn: func(struct{ Test1 *uint }) {}, - Err: nil, + Fn: func(struct{ Test1 *uint }) {}, + FnCtx: func(api.Ctx, struct{ Test1 *uint }) {}, + Err: nil, }, { Name: "1 valid *float64", Input: map[string]reflect.Type{ "Test1": reflect.TypeOf(new(float64)), }, - Fn: func(struct{ Test1 *float64 }) {}, - Err: nil, + Fn: func(struct{ Test1 *float64 }) {}, + FnCtx: func(api.Ctx, struct{ Test1 *float64 }) {}, + Err: nil, }, { Name: "1 valid *[]byte", Input: map[string]reflect.Type{ "Test1": reflect.TypeOf(new([]byte)), }, - Fn: func(struct{ Test1 *[]byte }) {}, - Err: nil, + Fn: func(struct{ Test1 *[]byte }) {}, + FnCtx: func(api.Ctx, struct{ Test1 *[]byte }) {}, + Err: nil, }, { Name: "1 valid *[]rune", Input: map[string]reflect.Type{ "Test1": reflect.TypeOf(new([]rune)), }, - Fn: func(struct{ Test1 *[]rune }) {}, - Err: nil, + Fn: func(struct{ Test1 *[]rune }) {}, + FnCtx: func(api.Ctx, struct{ Test1 *[]rune }) {}, + Err: nil, }, } for _, tcase := range tcases { t.Run(tcase.Name, func(t *testing.T) { + t.Parallel() + // mock spec s := spec{ Input: tcase.Input, Output: nil, } - err := s.checkInput(reflect.ValueOf(tcase.Fn)) - if err == nil && tcase.Err != nil { - t.Errorf("expected an error: '%s'", tcase.Err.Error()) - t.FailNow() - } - if err != nil && tcase.Err == nil { - t.Errorf("unexpected error: '%s'", err.Error()) - t.FailNow() - } - - if err != nil && tcase.Err != nil { - if !errors.Is(err, tcase.Err) { - t.Errorf("expected the error <%s> got <%s>", tcase.Err, err) + t.Run("with-context", func(t *testing.T) { + err := s.checkInput(reflect.TypeOf(tcase.FnCtx), 1) + if err == nil && tcase.Err != nil { + t.Errorf("expected an error: '%s'", tcase.Err.Error()) t.FailNow() } - } + if err != nil && tcase.Err == nil { + t.Errorf("unexpected error: '%s'", err.Error()) + t.FailNow() + } + + if err != nil && tcase.Err != nil { + if !errors.Is(err, tcase.Err) { + t.Errorf("expected the error <%s> got <%s>", tcase.Err, err) + t.FailNow() + } + } + }) + t.Run("without-context", func(t *testing.T) { + err := s.checkInput(reflect.TypeOf(tcase.Fn), 0) + if err == nil && tcase.Err != nil { + t.Errorf("expected an error: '%s'", tcase.Err.Error()) + t.FailNow() + } + if err != nil && tcase.Err == nil { + t.Errorf("unexpected error: '%s'", err.Error()) + t.FailNow() + } + + if err != nil && tcase.Err != nil { + if !errors.Is(err, tcase.Err) { + t.Errorf("expected the error <%s> got <%s>", tcase.Err, err) + t.FailNow() + } + } + }) }) } } @@ -322,13 +368,15 @@ func TestOutputCheck(t *testing.T) { for i, tcase := range tcases { t.Run(fmt.Sprintf("case.%d", i), func(t *testing.T) { + t.Parallel() + // mock spec s := spec{ Input: nil, Output: tcase.Output, } - err := s.checkOutput(reflect.ValueOf(tcase.Fn)) + err := s.checkOutput(reflect.TypeOf(tcase.Fn)) if err == nil && tcase.Err != nil { t.Errorf("expected an error: '%s'", tcase.Err.Error()) t.FailNow()