From f334d19ef4dc919242586fe009bfb7bc256c6c89 Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Sun, 18 Apr 2021 18:14:30 +0200 Subject: [PATCH 1/9] feat: add api context type --- api/context.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 api/context.go diff --git a/api/context.go b/api/context.go new file mode 100644 index 0000000..ba506d7 --- /dev/null +++ b/api/context.go @@ -0,0 +1,17 @@ +package api + +import ( + "net/http" +) + +// Ctx contains additional information for handlers +// +// usually input/output arguments built by aicra are sufficient +// but the Ctx lets you manage your request from scratch if required +// +// If required, set api.Ctx as the first argument of your handler; if you +// don't need it, only use standard input arguments and it will be ignored +type Ctx struct { + w http.ResponseWriter + r *http.Request +} -- 2.40.1 From 24be7c294e2a317d0bf76946cb49a621404adc33 Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Sun, 18 Apr 2021 18:26:37 +0200 Subject: [PATCH 2/9] test: dynamic func input --- internal/dynfunc/spec_test.go | 139 +++++++++++++++++++++++++++++++--- 1 file changed, 129 insertions(+), 10 deletions(-) diff --git a/internal/dynfunc/spec_test.go b/internal/dynfunc/spec_test.go index b8599bc..b361893 100644 --- a/internal/dynfunc/spec_test.go +++ b/internal/dynfunc/spec_test.go @@ -11,74 +11,193 @@ import ( func TestInputCheck(t *testing.T) { tcases := []struct { + Name string Input map[string]reflect.Type Fn interface{} Err error }{ - // no input { + Name: "no input 0 given", Input: map[string]reflect.Type{}, Fn: func() {}, Err: nil, }, - // func must have noarguments if none specified { + Name: "no input 1 given", + Input: map[string]reflect.Type{}, + Fn: func(int) {}, + Err: errUnexpectedInput, + }, + { + Name: "no input 2 given", Input: map[string]reflect.Type{}, Fn: func(int, string) {}, Err: errUnexpectedInput, }, - // missing input struct in func { + Name: "1 input 0 given", Input: map[string]reflect.Type{ "Test1": reflect.TypeOf(int(0)), }, Fn: func() {}, Err: errMissingHandlerArgumentParam, }, - // input not a struct { + Name: "1 input non-struct given", Input: map[string]reflect.Type{ "Test1": reflect.TypeOf(int(0)), }, Fn: func(int) {}, Err: errMissingParamArgument, }, - // unexported param name { + Name: "unexported input", Input: map[string]reflect.Type{ "test1": reflect.TypeOf(int(0)), }, Fn: func(struct{}) {}, Err: errUnexportedName, }, - // input field missing { + Name: "1 input empty struct given", Input: map[string]reflect.Type{ "Test1": reflect.TypeOf(int(0)), }, Fn: func(struct{}) {}, Err: errMissingParamFromConfig, }, - // input field invalid type { + Name: "1 input invalid given", Input: map[string]reflect.Type{ "Test1": reflect.TypeOf(int(0)), }, Fn: func(struct{ Test1 string }) {}, Err: errWrongParamTypeFromConfig, }, - // input field valid type { + Name: "1 input valid given", Input: map[string]reflect.Type{ "Test1": reflect.TypeOf(int(0)), }, Fn: func(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, + }, + { + Name: "1 input ptr invalid given", + Input: map[string]reflect.Type{ + "Test1": reflect.TypeOf(new(int)), + }, + Fn: func(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, + }, + { + Name: "1 input ptr valid given", + Input: map[string]reflect.Type{ + "Test1": reflect.TypeOf(new(int)), + }, + Fn: func(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, + }, + { + Name: "1 valid uint", + Input: map[string]reflect.Type{ + "Test1": reflect.TypeOf(uint(0)), + }, + Fn: func(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, + }, + { + Name: "1 valid []byte", + Input: map[string]reflect.Type{ + "Test1": reflect.TypeOf([]byte("")), + }, + Fn: func(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, + }, + { + Name: "1 valid *string", + Input: map[string]reflect.Type{ + "Test1": reflect.TypeOf(new(string)), + }, + Fn: func(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, + }, + { + Name: "1 valid *float64", + Input: map[string]reflect.Type{ + "Test1": reflect.TypeOf(new(float64)), + }, + Fn: func(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, + }, + { + Name: "1 valid *[]rune", + Input: map[string]reflect.Type{ + "Test1": reflect.TypeOf(new([]rune)), + }, + Fn: func(struct{ Test1 *[]rune }) {}, + Err: nil, + }, } - for i, tcase := range tcases { - t.Run(fmt.Sprintf("case.%d", i), func(t *testing.T) { + for _, tcase := range tcases { + t.Run(tcase.Name, func(t *testing.T) { // mock spec s := spec{ Input: tcase.Input, -- 2.40.1 From 0a55c2ee134f558019c67f8ecc38927a7285222c Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Sun, 18 Apr 2021 19:25:31 +0200 Subject: [PATCH 3/9] 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() -- 2.40.1 From 939ab2e57d63d78555aa37628a94768ecc10f48f Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Sun, 18 Apr 2021 19:31:40 +0200 Subject: [PATCH 4/9] fixup: expose api context fields --- api/context.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/context.go b/api/context.go index ba506d7..d8e390e 100644 --- a/api/context.go +++ b/api/context.go @@ -12,6 +12,6 @@ import ( // If required, set api.Ctx as the first argument of your handler; if you // don't need it, only use standard input arguments and it will be ignored type Ctx struct { - w http.ResponseWriter - r *http.Request + Res http.ResponseWriter + Req *http.Request } -- 2.40.1 From d6f8457274c2dc616b7f5006d420a261ef6b039f Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Sun, 18 Apr 2021 19:31:54 +0200 Subject: [PATCH 5/9] feat: pass optional context argument to handlers --- handler.go | 3 ++- internal/dynfunc/handler.go | 17 +++++++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/handler.go b/handler.go index b6feeb3..662daf3 100644 --- a/handler.go +++ b/handler.go @@ -51,7 +51,8 @@ func (s Handler) handleRequest(w http.ResponseWriter, r *http.Request) { } // 5. pass execution to the handler - var outData, outErr = handler.dyn.Handle(input.Data) + ctx := api.Ctx{Res: w, Req: r} + var outData, outErr = handler.dyn.Handle(ctx, input.Data) // 6. build res from returned data var res = api.EmptyResponse().WithError(outErr) diff --git a/internal/dynfunc/handler.go b/internal/dynfunc/handler.go index f7566c8..f47eed9 100644 --- a/internal/dynfunc/handler.go +++ b/internal/dynfunc/handler.go @@ -15,6 +15,8 @@ type Handler struct { fn interface{} // whether fn uses api.Ctx as 1st argument hasContext bool + // index in input arguments where the data struct must be + dataIndex int } // Build a handler from a service configuration and a dynamic function @@ -39,13 +41,11 @@ func Build(fn interface{}, service config.Service) (*Handler, error) { } h.hasContext = impl.NumIn() >= 1 && reflect.TypeOf(api.Ctx{}).AssignableTo(impl.In(0)) - - inputIndex := 0 if h.hasContext { - inputIndex = 1 + h.dataIndex = 1 } - if err := h.spec.checkInput(impl, inputIndex); err != nil { + if err := h.spec.checkInput(impl, h.dataIndex); err != nil { return nil, fmt.Errorf("input: %w", err) } if err := h.spec.checkOutput(impl); err != nil { @@ -56,14 +56,19 @@ func Build(fn interface{}, service config.Service) (*Handler, error) { } // Handle binds input @data into the dynamic function and returns map output -func (h *Handler) Handle(data map[string]interface{}) (map[string]interface{}, api.Err) { +func (h *Handler) Handle(ctx api.Ctx, data map[string]interface{}) (map[string]interface{}, api.Err) { var ert = reflect.TypeOf(api.Err{}) var fnv = reflect.ValueOf(h.fn) callArgs := []reflect.Value{} + // bind context if used in handler + if h.hasContext { + callArgs = append(callArgs, reflect.ValueOf(ctx)) + } + // bind input data - if fnv.Type().NumIn() > 0 { + if fnv.Type().NumIn() > h.dataIndex { // create zero value struct callStructPtr := reflect.New(fnv.Type().In(0)) callStruct := callStructPtr.Elem() -- 2.40.1 From 1245861be72021a503378e9d08fc4522a6f3d739 Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Mon, 19 Apr 2021 18:46:18 +0200 Subject: [PATCH 6/9] test: builder --- builder.go | 5 +- builder_test.go | 352 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 355 insertions(+), 2 deletions(-) create mode 100644 builder_test.go diff --git a/builder.go b/builder.go index f65fefa..224ce7c 100644 --- a/builder.go +++ b/builder.go @@ -26,17 +26,18 @@ type apiHandler struct { } // AddType adds an available datatype to the api definition -func (b *Builder) AddType(t datatype.T) { +func (b *Builder) AddType(t datatype.T) error { if b.conf == nil { b.conf = &config.Server{} } if b.conf.Services != nil { - panic(errLateType) + return errLateType } if b.conf.Types == nil { b.conf.Types = make([]datatype.T, 0) } b.conf.Types = append(b.conf.Types, t) + return nil } // Use adds an http adapter (middleware) diff --git a/builder_test.go b/builder_test.go new file mode 100644 index 0000000..8e3358d --- /dev/null +++ b/builder_test.go @@ -0,0 +1,352 @@ +package aicra + +import ( + "bytes" + "context" + "errors" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "git.xdrm.io/go/aicra/api" + "git.xdrm.io/go/aicra/datatype/builtin" +) + +func addBuiltinTypes(b *Builder) error { + if err := b.AddType(builtin.AnyDataType{}); err != nil { + return err + } + if err := b.AddType(builtin.BoolDataType{}); err != nil { + return err + } + if err := b.AddType(builtin.FloatDataType{}); err != nil { + return err + } + if err := b.AddType(builtin.IntDataType{}); err != nil { + return err + } + if err := b.AddType(builtin.StringDataType{}); err != nil { + return err + } + if err := b.AddType(builtin.UintDataType{}); err != nil { + return err + } + return nil +} + +func TestAddType(t *testing.T) { + builder := &Builder{} + err := builder.AddType(builtin.BoolDataType{}) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + err = builder.Setup(strings.NewReader("[]")) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + err = builder.AddType(builtin.FloatDataType{}) + if err != errLateType { + t.Fatalf("expected <%v> got <%v>", errLateType, err) + } +} + +func TestUse(t *testing.T) { + builder := &Builder{} + if err := addBuiltinTypes(builder); err != nil { + t.Fatalf("unexpected error <%v>", err) + } + + // build @n middlewares that take data from context and increment it + n := 1024 + + type ckey int + const key ckey = 0 + + middleware := func(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + newr := r + + // first time -> store 1 + value := r.Context().Value(key) + if value == nil { + newr = r.WithContext(context.WithValue(r.Context(), key, int(1))) + next(w, newr) + return + } + + // get value and increment + cast, ok := value.(int) + if !ok { + t.Fatalf("value is not an int") + } + cast++ + newr = r.WithContext(context.WithValue(r.Context(), key, cast)) + next(w, newr) + } + } + + // add middleware @n times + for i := 0; i < n; i++ { + builder.Use(middleware) + } + + config := strings.NewReader(`[ { "method": "GET", "path": "/path", "scope": [[]], "info": "info", "in": {}, "out": {} } ]`) + err := builder.Setup(config) + if err != nil { + t.Fatalf("setup: unexpected error <%v>", err) + } + + pathHandler := func(ctx api.Ctx) (*struct{}, api.Err) { + // write value from middlewares into response + value := ctx.Req.Context().Value(key) + if value == nil { + t.Fatalf("nothing found in context") + } + cast, ok := value.(int) + if !ok { + t.Fatalf("cannot cast context data to int") + } + // write to response + ctx.Res.Write([]byte(fmt.Sprintf("#%d#", cast))) + + return nil, api.ErrSuccess + } + + if err := builder.Bind(http.MethodGet, "/path", pathHandler); err != nil { + t.Fatalf("bind: unexpected error <%v>", err) + } + + handler, err := builder.Build() + if err != nil { + t.Fatalf("build: unexpected error <%v>", err) + } + + response := httptest.NewRecorder() + request := httptest.NewRequest(http.MethodGet, "/path", &bytes.Buffer{}) + + // test request + handler.ServeHTTP(response, request) + if response.Body == nil { + t.Fatalf("response has no body") + } + token := fmt.Sprintf("#%d#", n) + if !strings.Contains(response.Body.String(), token) { + t.Fatalf("expected '%s' to be in response <%s>", token, response.Body.String()) + } + +} + +func TestBind(t *testing.T) { + tcases := []struct { + Name string + Config string + HandlerMethod string + HandlerPath string + HandlerFn interface{} // not bound if nil + BindErr error + BuildErr error + }{ + { + Name: "none required none provided", + Config: "[]", + HandlerMethod: "", + HandlerPath: "", + HandlerFn: nil, + BindErr: nil, + BuildErr: nil, + }, + { + Name: "none required 1 provided", + Config: "[]", + HandlerMethod: "", + HandlerPath: "", + HandlerFn: func() (*struct{}, api.Err) { return nil, api.ErrSuccess }, + BindErr: errUnknownService, + BuildErr: nil, + }, + { + Name: "1 required none provided", + Config: `[ + { + "method": "GET", + "path": "/path", + "scope": [[]], + "info": "info", + "in": {}, + "out": {} + } + ]`, + HandlerMethod: "", + HandlerPath: "", + HandlerFn: nil, + BindErr: nil, + BuildErr: errMissingHandler, + }, + { + Name: "1 required wrong method provided", + Config: `[ + { + "method": "GET", + "path": "/path", + "scope": [[]], + "info": "info", + "in": {}, + "out": {} + } + ]`, + HandlerMethod: http.MethodPost, + HandlerPath: "/path", + HandlerFn: func() (*struct{}, api.Err) { return nil, api.ErrSuccess }, + BindErr: errUnknownService, + BuildErr: errMissingHandler, + }, + { + Name: "1 required wrong path provided", + Config: `[ + { + "method": "GET", + "path": "/path", + "scope": [[]], + "info": "info", + "in": {}, + "out": {} + } + ]`, + HandlerMethod: http.MethodGet, + HandlerPath: "/paths", + HandlerFn: func() (*struct{}, api.Err) { return nil, api.ErrSuccess }, + BindErr: errUnknownService, + BuildErr: errMissingHandler, + }, + { + Name: "1 required valid provided", + Config: `[ + { + "method": "GET", + "path": "/path", + "scope": [[]], + "info": "info", + "in": {}, + "out": {} + } + ]`, + HandlerMethod: http.MethodGet, + HandlerPath: "/path", + HandlerFn: func() (*struct{}, api.Err) { return nil, api.ErrSuccess }, + BindErr: nil, + BuildErr: nil, + }, + { + Name: "1 required with int", + Config: `[ + { + "method": "GET", + "path": "/path", + "scope": [[]], + "info": "info", + "in": { + "id": { "info": "info", "type": "int", "name": "Name" } + }, + "out": {} + } + ]`, + HandlerMethod: http.MethodGet, + HandlerPath: "/path", + HandlerFn: func(struct{ Name int }) (*struct{}, api.Err) { return nil, api.ErrSuccess }, + BindErr: nil, + BuildErr: nil, + }, + { + Name: "1 required with uint", + Config: `[ + { + "method": "GET", + "path": "/path", + "scope": [[]], + "info": "info", + "in": { + "id": { "info": "info", "type": "uint", "name": "Name" } + }, + "out": {} + } + ]`, + HandlerMethod: http.MethodGet, + HandlerPath: "/path", + HandlerFn: func(struct{ Name uint }) (*struct{}, api.Err) { return nil, api.ErrSuccess }, + BindErr: nil, + BuildErr: nil, + }, + { + Name: "1 required with string", + Config: `[ + { + "method": "GET", + "path": "/path", + "scope": [[]], + "info": "info", + "in": { + "id": { "info": "info", "type": "string", "name": "Name" } + }, + "out": {} + } + ]`, + HandlerMethod: http.MethodGet, + HandlerPath: "/path", + HandlerFn: func(struct{ Name string }) (*struct{}, api.Err) { return nil, api.ErrSuccess }, + BindErr: nil, + BuildErr: nil, + }, + { + Name: "1 required with bool", + Config: `[ + { + "method": "GET", + "path": "/path", + "scope": [[]], + "info": "info", + "in": { + "id": { "info": "info", "type": "bool", "name": "Name" } + }, + "out": {} + } + ]`, + HandlerMethod: http.MethodGet, + HandlerPath: "/path", + HandlerFn: func(struct{ Name bool }) (*struct{}, api.Err) { return nil, api.ErrSuccess }, + BindErr: nil, + BuildErr: nil, + }, + } + + for _, tcase := range tcases { + t.Run(tcase.Name, func(t *testing.T) { + t.Parallel() + + builder := &Builder{} + + if err := addBuiltinTypes(builder); err != nil { + t.Fatalf("add built-in types: %s", err) + } + + err := builder.Setup(strings.NewReader(tcase.Config)) + if err != nil { + t.Fatalf("setup: unexpected error <%v>", err) + } + + if tcase.HandlerFn != nil { + err := builder.Bind(tcase.HandlerMethod, tcase.HandlerPath, tcase.HandlerFn) + if !errors.Is(err, tcase.BindErr) { + t.Fatalf("bind: expected <%v> got <%v>", tcase.BindErr, err) + } + } + + _, err = builder.Build() + if !errors.Is(err, tcase.BuildErr) { + t.Fatalf("build: expected <%v> got <%v>", tcase.BuildErr, err) + } + + }) + } +} -- 2.40.1 From e44dab4bc9e173663d81abd11d3ef46a98816a76 Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Mon, 19 Apr 2021 19:55:00 +0200 Subject: [PATCH 7/9] fixup: remove HasContext from spec --- internal/dynfunc/spec.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/internal/dynfunc/spec.go b/internal/dynfunc/spec.go index 9f487f4..8f43ccd 100644 --- a/internal/dynfunc/spec.go +++ b/internal/dynfunc/spec.go @@ -12,8 +12,6 @@ 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 -- 2.40.1 From b88a4439c8799404c0603dc5bee4288b4db962f1 Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Mon, 19 Apr 2021 22:15:34 +0200 Subject: [PATCH 8/9] fixup: update comment for optional api.Ctx --- internal/dynfunc/handler.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/dynfunc/handler.go b/internal/dynfunc/handler.go index f47eed9..4c544bc 100644 --- a/internal/dynfunc/handler.go +++ b/internal/dynfunc/handler.go @@ -26,6 +26,7 @@ type Handler struct { // - `outputStruct` is a struct{} containing a field for each service output (with valid reflect.Type) // // Special cases: +// - a first optional input parameter of type `api.Ctx` can be added // - it there is no input, `inputStruct` must be omitted // - it there is no output, `outputStruct` must be omitted func Build(fn interface{}, service config.Service) (*Handler, error) { -- 2.40.1 From af106acd3fe835740f65cc09b611fbcba8b9f9b2 Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Mon, 19 Apr 2021 23:34:31 +0200 Subject: [PATCH 9/9] refactor: test: dynamic function handler --- internal/dynfunc/handler.go | 13 +- internal/dynfunc/handler_test.go | 173 ++++++++++++++++++ internal/dynfunc/{spec.go => signature.go} | 11 +- .../{spec_test.go => signature_test.go} | 4 +- 4 files changed, 188 insertions(+), 13 deletions(-) create mode 100644 internal/dynfunc/handler_test.go rename internal/dynfunc/{spec.go => signature.go} (90%) rename internal/dynfunc/{spec_test.go => signature_test.go} (99%) 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, } -- 2.40.1