feature: add optional context to handlers #19
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
if len(s.Input) == 0 {
|
||||
if fnt.NumIn() > 0 {
|
||||
// missing arguments
|
||||
if impl.NumIn() > requiredInput {
|
||||
return errUnexpectedInput
|
||||
}
|
||||
|
||||
// none required
|
||||
if len(s.Input) == 0 {
|
||||
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
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue