Merge pull request 'feature: add optional context to handlers' (#19) from feature/context into 0.3.0
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details

An optional first input argument of type api.Ctx to handlers to access standard request/response
This commit is contained in:
Adrien Marquès 2021-05-10 14:42:57 +00:00
commit e3d24ae1ef
9 changed files with 999 additions and 266 deletions

17
api/context.go Normal file
View File

@ -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
}

View File

@ -26,17 +26,18 @@ type apiHandler struct {
} }
// AddType adds an available datatype to the api definition // 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 { if b.conf == nil {
b.conf = &config.Server{} b.conf = &config.Server{}
} }
if b.conf.Services != nil { if b.conf.Services != nil {
panic(errLateType) return errLateType
} }
if b.conf.Types == nil { if b.conf.Types == nil {
b.conf.Types = make([]datatype.T, 0) b.conf.Types = make([]datatype.T, 0)
} }
b.conf.Types = append(b.conf.Types, t) b.conf.Types = append(b.conf.Types, t)
return nil
} }
// Use adds an http adapter (middleware) // Use adds an http adapter (middleware)

352
builder_test.go Normal file
View File

@ -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)
}
})
}
}

View File

@ -51,7 +51,8 @@ func (s Handler) handleRequest(w http.ResponseWriter, r *http.Request) {
} }
// 5. pass execution to the handler // 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 // 6. build res from returned data
var res = api.EmptyResponse().WithError(outErr) var res = api.EmptyResponse().WithError(outErr)

View File

@ -11,8 +11,12 @@ import (
// Handler represents a dynamic api handler // Handler represents a dynamic api handler
type Handler struct { type Handler struct {
spec spec spec *signature
fn interface{} 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 // 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) // - `outputStruct` is a struct{} containing a field for each service output (with valid reflect.Type)
// //
// Special cases: // 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 input, `inputStruct` must be omitted
// - it there is no output, `outputStruct` must be omitted // - it there is no output, `outputStruct` must be omitted
func Build(fn interface{}, service config.Service) (*Handler, error) { func Build(fn interface{}, service config.Service) (*Handler, error) {
h := &Handler{ h := &Handler{
spec: makeSpec(service), spec: signatureFromService(service),
fn: fn, fn: fn,
} }
fnv := reflect.ValueOf(fn) impl := reflect.TypeOf(fn)
if fnv.Type().Kind() != reflect.Func { if impl.Kind() != reflect.Func {
return nil, errHandlerNotFunc 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) 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) 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 // 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 ert = reflect.TypeOf(api.Err{})
var fnv = reflect.ValueOf(h.fn) var fnv = reflect.ValueOf(h.fn)
callArgs := []reflect.Value{} callArgs := []reflect.Value{}
// bind context if used in handler
if h.hasContext {
callArgs = append(callArgs, reflect.ValueOf(ctx))
}
// bind input data // bind input data
if fnv.Type().NumIn() > 0 { if fnv.Type().NumIn() > h.dataIndex {
// create zero value struct // create zero value struct
callStructPtr := reflect.New(fnv.Type().In(0)) callStructPtr := reflect.New(fnv.Type().In(0))
callStruct := callStructPtr.Elem() callStruct := callStructPtr.Elem()
@ -67,8 +82,8 @@ func (h *Handler) Handle(data map[string]interface{}) (map[string]interface{}, a
} }
// get value from @data // get value from @data
value, inData := data[name] value, provided := data[name]
if !inData { if !provided {
continue continue
} }
@ -79,7 +94,7 @@ func (h *Handler) Handle(data map[string]interface{}) (map[string]interface{}, a
var ptrType = field.Type().Elem() var ptrType = field.Type().Elem()
if !refvalue.Type().ConvertibleTo(ptrType) { 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 return nil, api.ErrUncallableService
} }
@ -89,12 +104,13 @@ func (h *Handler) Handle(data map[string]interface{}) (map[string]interface{}, a
field.Set(ptr) field.Set(ptr)
continue continue
} }
if !reflect.ValueOf(value).Type().ConvertibleTo(field.Type()) { if !reflect.ValueOf(value).Type().ConvertibleTo(field.Type()) {
log.Printf("Cannot convert %v into %v", reflect.ValueOf(value).Type(), field.Type()) log.Printf("Cannot convert %v into %v", reflect.ValueOf(value).Type(), field.Type())
return nil, api.ErrUncallableService return nil, api.ErrUncallableService
} }
field.Set(reflect.ValueOf(value).Convert(field.Type())) field.Set(refvalue.Convert(field.Type()))
} }
callArgs = append(callArgs, callStruct) callArgs = append(callArgs, callStruct)
} }

View File

@ -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)
}
}
})
}
}

View File

@ -9,14 +9,15 @@ import (
"git.xdrm.io/go/aicra/internal/config" "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 Input map[string]reflect.Type
Output map[string]reflect.Type Output map[string]reflect.Type
} }
// builds a spec from the configuration service // builds a spec from the configuration service
func makeSpec(service config.Service) spec { func signatureFromService(service config.Service) *signature {
spec := spec{ s := &signature{
Input: make(map[string]reflect.Type), Input: make(map[string]reflect.Type),
Output: 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 // make a pointer if optional
if param.Optional { if param.Optional {
spec.Input[param.Rename] = reflect.PtrTo(param.ExtractType) s.Input[param.Rename] = reflect.PtrTo(param.ExtractType)
continue continue
} }
spec.Input[param.Rename] = param.ExtractType s.Input[param.Rename] = param.ExtractType
} }
for _, param := range service.Output { for _, param := range service.Output {
if len(param.Rename) < 1 { if len(param.Rename) < 1 {
continue continue
} }
spec.Output[param.Rename] = param.ExtractType s.Output[param.Rename] = param.ExtractType
} }
return spec return s
} }
// checks for HandlerFn input arguments // checks for HandlerFn input arguments
func (s spec) checkInput(fnv reflect.Value) error { func (s *signature) checkInput(impl reflect.Type, index int) error {
fnt := fnv.Type() var requiredInput, structIndex = index, index
if len(s.Input) > 0 { // arguments struct
requiredInput++
}
// no input -> ok // missing arguments
if len(s.Input) == 0 { if impl.NumIn() > requiredInput {
if fnt.NumIn() > 0 {
return errUnexpectedInput return errUnexpectedInput
} }
// none required
if len(s.Input) == 0 {
return nil return nil
} }
if fnt.NumIn() != 1 { // too much arguments
if impl.NumIn() != requiredInput {
return errMissingHandlerArgumentParam return errMissingHandlerArgumentParam
} }
// arg must be a struct // arg must be a struct
structArg := fnt.In(0) structArg := impl.In(structIndex)
if structArg.Kind() != reflect.Struct { if structArg.Kind() != reflect.Struct {
return errMissingParamArgument return errMissingParamArgument
} }
@ -85,14 +92,13 @@ func (s spec) checkInput(fnv reflect.Value) error {
} }
// checks for HandlerFn output arguments // checks for HandlerFn output arguments
func (s spec) checkOutput(fnv reflect.Value) error { func (s signature) checkOutput(impl reflect.Type) error {
fnt := fnv.Type() if impl.NumOut() < 1 {
if fnt.NumOut() < 1 {
return errMissingHandlerOutput return errMissingHandlerOutput
} }
// last output must be api.Err // 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)) { if !errOutput.AssignableTo(reflect.TypeOf(api.ErrUnknown)) {
return errMissingHandlerErrorOutput return errMissingHandlerErrorOutput
} }
@ -102,12 +108,12 @@ func (s spec) checkOutput(fnv reflect.Value) error {
return nil return nil
} }
if fnt.NumOut() != 2 { if impl.NumOut() != 2 {
return errMissingParamOutput return errMissingParamOutput
} }
// fail if first output is not a pointer to struct // fail if first output is not a pointer to struct
structOutputPtr := fnt.Out(0) structOutputPtr := impl.Out(0)
if structOutputPtr.Kind() != reflect.Ptr { if structOutputPtr.Kind() != reflect.Ptr {
return errMissingParamOutput return errMissingParamOutput
} }

View File

@ -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()
}
}
})
}
}

View File

@ -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()
}
}
})
}
}