diff --git a/api/context.go b/api/context.go new file mode 100644 index 0000000..d8e390e --- /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 { + Res http.ResponseWriter + Req *http.Request +} 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) + } + + }) + } +} 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 4e36b71..634cfc6 100644 --- a/internal/dynfunc/handler.go +++ b/internal/dynfunc/handler.go @@ -11,8 +11,12 @@ 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 + // index in input arguments where the data struct must be + dataIndex int } // Build a handler from a service configuration and a dynamic function @@ -22,24 +26,30 @@ 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) { h := &Handler{ - spec: makeSpec(service), + spec: signatureFromService(service), 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)) + if h.hasContext { + h.dataIndex = 1 + } + + if err := h.spec.checkInput(impl, h.dataIndex); 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) } @@ -47,14 +57,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() @@ -67,8 +82,8 @@ func (h *Handler) Handle(data map[string]interface{}) (map[string]interface{}, a } // get value from @data - value, inData := data[name] - if !inData { + value, provided := data[name] + if !provided { continue } @@ -79,7 +94,7 @@ func (h *Handler) Handle(data map[string]interface{}) (map[string]interface{}, a 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 } @@ -89,12 +104,13 @@ func (h *Handler) Handle(data map[string]interface{}) (map[string]interface{}, a 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 72% rename from internal/dynfunc/spec.go rename to internal/dynfunc/signature.go index 571c9ae..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 { - spec := spec{ +func signatureFromService(service config.Service) *signature { + s := &signature{ Input: make(map[string]reflect.Type), Output: make(map[string]reflect.Type), } @@ -27,40 +28,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 *signature) 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 +92,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 signature) 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 +108,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/signature_test.go b/internal/dynfunc/signature_test.go new file mode 100644 index 0000000..8860a92 --- /dev/null +++ b/internal/dynfunc/signature_test.go @@ -0,0 +1,397 @@ +package dynfunc + +import ( + "errors" + "fmt" + "reflect" + "testing" + + "git.xdrm.io/go/aicra/api" +) + +func TestInputCheck(t *testing.T) { + tcases := []struct { + 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, + }, + { + Name: "1 input 0 given", + Input: map[string]reflect.Type{ + "Test1": reflect.TypeOf(int(0)), + }, + 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) {}, + FnCtx: func(api.Ctx, int) {}, + Err: errMissingParamArgument, + }, + { + Name: "unexported input", + Input: map[string]reflect.Type{ + "test1": reflect.TypeOf(int(0)), + }, + 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{}) {}, + 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 }) {}, + 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 }) {}, + 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{}) {}, + 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 }) {}, + 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 }) {}, + 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 }) {}, + 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 }) {}, + 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 }) {}, + 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 }) {}, + 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 }) {}, + 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 }) {}, + 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 }) {}, + 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 }) {}, + 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 }) {}, + 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 }) {}, + 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 }) {}, + 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 := signature{ + Input: tcase.Input, + Output: nil, + } + + 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() + } + } + }) + }) + } +} + +func TestOutputCheck(t *testing.T) { + tcases := []struct { + Output map[string]reflect.Type + Fn interface{} + Err error + }{ + // no input -> missing api.Err + { + Output: map[string]reflect.Type{}, + Fn: func() {}, + Err: errMissingHandlerOutput, + }, + // no input -> with last type not api.Err + { + Output: map[string]reflect.Type{}, + Fn: func() bool { return true }, + Err: errMissingHandlerErrorOutput, + }, + // no input -> with api.Err + { + Output: map[string]reflect.Type{}, + Fn: func() api.Err { return api.ErrSuccess }, + Err: nil, + }, + // func can have output if not specified + { + Output: map[string]reflect.Type{}, + Fn: func() (*struct{}, api.Err) { return nil, api.ErrSuccess }, + Err: nil, + }, + // missing output struct in func + { + Output: map[string]reflect.Type{ + "Test1": reflect.TypeOf(int(0)), + }, + Fn: func() api.Err { return api.ErrSuccess }, + Err: errMissingParamOutput, + }, + // output not a pointer + { + Output: map[string]reflect.Type{ + "Test1": reflect.TypeOf(int(0)), + }, + Fn: func() (int, api.Err) { return 0, api.ErrSuccess }, + Err: errMissingParamOutput, + }, + // output not a pointer to struct + { + Output: map[string]reflect.Type{ + "Test1": reflect.TypeOf(int(0)), + }, + Fn: func() (*int, api.Err) { return nil, api.ErrSuccess }, + Err: errMissingParamOutput, + }, + // unexported param name + { + Output: map[string]reflect.Type{ + "test1": reflect.TypeOf(int(0)), + }, + Fn: func() (*struct{}, api.Err) { return nil, api.ErrSuccess }, + Err: errUnexportedName, + }, + // output field missing + { + Output: map[string]reflect.Type{ + "Test1": reflect.TypeOf(int(0)), + }, + Fn: func() (*struct{}, api.Err) { return nil, api.ErrSuccess }, + Err: errMissingParamFromConfig, + }, + // output field invalid type + { + Output: map[string]reflect.Type{ + "Test1": reflect.TypeOf(int(0)), + }, + Fn: func() (*struct{ Test1 string }, api.Err) { return nil, api.ErrSuccess }, + Err: errWrongParamTypeFromConfig, + }, + // output field valid type + { + Output: map[string]reflect.Type{ + "Test1": reflect.TypeOf(int(0)), + }, + Fn: func() (*struct{ Test1 int }, api.Err) { return nil, api.ErrSuccess }, + Err: nil, + }, + // ignore type check on nil type + { + Output: map[string]reflect.Type{ + "Test1": nil, + }, + Fn: func() (*struct{ Test1 int }, api.Err) { return nil, api.ErrSuccess }, + Err: nil, + }, + } + + for i, tcase := range tcases { + t.Run(fmt.Sprintf("case.%d", i), func(t *testing.T) { + t.Parallel() + + // mock spec + s := signature{ + Input: nil, + Output: tcase.Output, + } + + err := s.checkOutput(reflect.TypeOf(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.FailNow() + } + } + }) + } +} diff --git a/internal/dynfunc/spec_test.go b/internal/dynfunc/spec_test.go deleted file mode 100644 index b8599bc..0000000 --- a/internal/dynfunc/spec_test.go +++ /dev/null @@ -1,230 +0,0 @@ -package dynfunc - -import ( - "errors" - "fmt" - "reflect" - "testing" - - "git.xdrm.io/go/aicra/api" -) - -func TestInputCheck(t *testing.T) { - tcases := []struct { - Input map[string]reflect.Type - Fn interface{} - Err error - }{ - // no input - { - Input: map[string]reflect.Type{}, - Fn: func() {}, - Err: nil, - }, - // func must have noarguments if none specified - { - Input: map[string]reflect.Type{}, - Fn: func(int, string) {}, - Err: errUnexpectedInput, - }, - // missing input struct in func - { - Input: map[string]reflect.Type{ - "Test1": reflect.TypeOf(int(0)), - }, - Fn: func() {}, - Err: errMissingHandlerArgumentParam, - }, - // input not a struct - { - Input: map[string]reflect.Type{ - "Test1": reflect.TypeOf(int(0)), - }, - Fn: func(int) {}, - Err: errMissingParamArgument, - }, - // unexported param name - { - Input: map[string]reflect.Type{ - "test1": reflect.TypeOf(int(0)), - }, - Fn: func(struct{}) {}, - Err: errUnexportedName, - }, - // input field missing - { - Input: map[string]reflect.Type{ - "Test1": reflect.TypeOf(int(0)), - }, - Fn: func(struct{}) {}, - Err: errMissingParamFromConfig, - }, - // input field invalid type - { - Input: map[string]reflect.Type{ - "Test1": reflect.TypeOf(int(0)), - }, - Fn: func(struct{ Test1 string }) {}, - Err: errWrongParamTypeFromConfig, - }, - // input field valid type - { - Input: map[string]reflect.Type{ - "Test1": reflect.TypeOf(int(0)), - }, - Fn: func(struct{ Test1 int }) {}, - Err: nil, - }, - } - - for i, tcase := range tcases { - t.Run(fmt.Sprintf("case.%d", i), func(t *testing.T) { - // 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.FailNow() - } - } - }) - } -} - -func TestOutputCheck(t *testing.T) { - tcases := []struct { - Output map[string]reflect.Type - Fn interface{} - Err error - }{ - // no input -> missing api.Err - { - Output: map[string]reflect.Type{}, - Fn: func() {}, - Err: errMissingHandlerOutput, - }, - // no input -> with last type not api.Err - { - Output: map[string]reflect.Type{}, - Fn: func() bool { return true }, - Err: errMissingHandlerErrorOutput, - }, - // no input -> with api.Err - { - Output: map[string]reflect.Type{}, - Fn: func() api.Err { return api.ErrSuccess }, - Err: nil, - }, - // func can have output if not specified - { - Output: map[string]reflect.Type{}, - Fn: func() (*struct{}, api.Err) { return nil, api.ErrSuccess }, - Err: nil, - }, - // missing output struct in func - { - Output: map[string]reflect.Type{ - "Test1": reflect.TypeOf(int(0)), - }, - Fn: func() api.Err { return api.ErrSuccess }, - Err: errMissingParamOutput, - }, - // output not a pointer - { - Output: map[string]reflect.Type{ - "Test1": reflect.TypeOf(int(0)), - }, - Fn: func() (int, api.Err) { return 0, api.ErrSuccess }, - Err: errMissingParamOutput, - }, - // output not a pointer to struct - { - Output: map[string]reflect.Type{ - "Test1": reflect.TypeOf(int(0)), - }, - Fn: func() (*int, api.Err) { return nil, api.ErrSuccess }, - Err: errMissingParamOutput, - }, - // unexported param name - { - Output: map[string]reflect.Type{ - "test1": reflect.TypeOf(int(0)), - }, - Fn: func() (*struct{}, api.Err) { return nil, api.ErrSuccess }, - Err: errUnexportedName, - }, - // output field missing - { - Output: map[string]reflect.Type{ - "Test1": reflect.TypeOf(int(0)), - }, - Fn: func() (*struct{}, api.Err) { return nil, api.ErrSuccess }, - Err: errMissingParamFromConfig, - }, - // output field invalid type - { - Output: map[string]reflect.Type{ - "Test1": reflect.TypeOf(int(0)), - }, - Fn: func() (*struct{ Test1 string }, api.Err) { return nil, api.ErrSuccess }, - Err: errWrongParamTypeFromConfig, - }, - // output field valid type - { - Output: map[string]reflect.Type{ - "Test1": reflect.TypeOf(int(0)), - }, - Fn: func() (*struct{ Test1 int }, api.Err) { return nil, api.ErrSuccess }, - Err: nil, - }, - // ignore type check on nil type - { - Output: map[string]reflect.Type{ - "Test1": nil, - }, - Fn: func() (*struct{ Test1 int }, api.Err) { return nil, api.ErrSuccess }, - Err: nil, - }, - } - - for i, tcase := range tcases { - t.Run(fmt.Sprintf("case.%d", i), func(t *testing.T) { - // mock spec - s := spec{ - Input: nil, - Output: tcase.Output, - } - - err := s.checkOutput(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.FailNow() - } - } - }) - } -}