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