refactor: handler signature an middlewares for an idiomatic solution #24

Merged
xdrm-brackets merged 6 commits from refactor/api-context into 0.4.0 2021-06-20 07:51:42 +00:00
8 changed files with 229 additions and 235 deletions
Showing only changes of commit 53dfc8f679 - Show all commits

View File

@ -72,7 +72,7 @@ func TestBind(t *testing.T) {
Config: "[]", Config: "[]",
HandlerMethod: "", HandlerMethod: "",
HandlerPath: "", HandlerPath: "",
HandlerFn: func() (*struct{}, api.Err) { return nil, api.ErrSuccess }, HandlerFn: func(*api.Context) (*struct{}, api.Err) { return nil, api.ErrSuccess },
BindErr: errUnknownService, BindErr: errUnknownService,
BuildErr: nil, BuildErr: nil,
}, },
@ -108,7 +108,7 @@ func TestBind(t *testing.T) {
]`, ]`,
HandlerMethod: http.MethodPost, HandlerMethod: http.MethodPost,
HandlerPath: "/path", HandlerPath: "/path",
HandlerFn: func() (*struct{}, api.Err) { return nil, api.ErrSuccess }, HandlerFn: func(*api.Context) (*struct{}, api.Err) { return nil, api.ErrSuccess },
BindErr: errUnknownService, BindErr: errUnknownService,
BuildErr: errMissingHandler, BuildErr: errMissingHandler,
}, },
@ -126,7 +126,7 @@ func TestBind(t *testing.T) {
]`, ]`,
HandlerMethod: http.MethodGet, HandlerMethod: http.MethodGet,
HandlerPath: "/paths", HandlerPath: "/paths",
HandlerFn: func() (*struct{}, api.Err) { return nil, api.ErrSuccess }, HandlerFn: func(*api.Context) (*struct{}, api.Err) { return nil, api.ErrSuccess },
BindErr: errUnknownService, BindErr: errUnknownService,
BuildErr: errMissingHandler, BuildErr: errMissingHandler,
}, },
@ -144,7 +144,7 @@ func TestBind(t *testing.T) {
]`, ]`,
HandlerMethod: http.MethodGet, HandlerMethod: http.MethodGet,
HandlerPath: "/path", HandlerPath: "/path",
HandlerFn: func() (*struct{}, api.Err) { return nil, api.ErrSuccess }, HandlerFn: func(*api.Context) (*struct{}, api.Err) { return nil, api.ErrSuccess },
BindErr: nil, BindErr: nil,
BuildErr: nil, BuildErr: nil,
}, },
@ -164,7 +164,7 @@ func TestBind(t *testing.T) {
]`, ]`,
HandlerMethod: http.MethodGet, HandlerMethod: http.MethodGet,
HandlerPath: "/path", HandlerPath: "/path",
HandlerFn: func(struct{ Name int }) (*struct{}, api.Err) { return nil, api.ErrSuccess }, HandlerFn: func(*api.Context, struct{ Name int }) (*struct{}, api.Err) { return nil, api.ErrSuccess },
BindErr: nil, BindErr: nil,
BuildErr: nil, BuildErr: nil,
}, },
@ -184,7 +184,7 @@ func TestBind(t *testing.T) {
]`, ]`,
HandlerMethod: http.MethodGet, HandlerMethod: http.MethodGet,
HandlerPath: "/path", HandlerPath: "/path",
HandlerFn: func(struct{ Name uint }) (*struct{}, api.Err) { return nil, api.ErrSuccess }, HandlerFn: func(*api.Context, struct{ Name uint }) (*struct{}, api.Err) { return nil, api.ErrSuccess },
BindErr: nil, BindErr: nil,
BuildErr: nil, BuildErr: nil,
}, },
@ -204,7 +204,7 @@ func TestBind(t *testing.T) {
]`, ]`,
HandlerMethod: http.MethodGet, HandlerMethod: http.MethodGet,
HandlerPath: "/path", HandlerPath: "/path",
HandlerFn: func(struct{ Name string }) (*struct{}, api.Err) { return nil, api.ErrSuccess }, HandlerFn: func(*api.Context, struct{ Name string }) (*struct{}, api.Err) { return nil, api.ErrSuccess },
BindErr: nil, BindErr: nil,
BuildErr: nil, BuildErr: nil,
}, },
@ -224,7 +224,7 @@ func TestBind(t *testing.T) {
]`, ]`,
HandlerMethod: http.MethodGet, HandlerMethod: http.MethodGet,
HandlerPath: "/path", HandlerPath: "/path",
HandlerFn: func(struct{ Name bool }) (*struct{}, api.Err) { return nil, api.ErrSuccess }, HandlerFn: func(*api.Context, struct{ Name bool }) (*struct{}, api.Err) { return nil, api.ErrSuccess },
BindErr: nil, BindErr: nil,
BuildErr: nil, BuildErr: nil,
}, },

View File

@ -1,12 +1,14 @@
package aicra package aicra
import ( import (
"context"
"fmt" "fmt"
"net/http" "net/http"
"strings" "strings"
"git.xdrm.io/go/aicra/api" "git.xdrm.io/go/aicra/api"
"git.xdrm.io/go/aicra/internal/config" "git.xdrm.io/go/aicra/internal/config"
"git.xdrm.io/go/aicra/internal/ctx"
"git.xdrm.io/go/aicra/internal/reqdata" "git.xdrm.io/go/aicra/internal/reqdata"
) )
@ -94,11 +96,17 @@ func (s Handler) resolve(w http.ResponseWriter, r *http.Request) {
} }
func (s *Handler) handle(input *reqdata.T, handler *apiHandler, service *config.Service, w http.ResponseWriter, r *http.Request) { func (s *Handler) handle(input *reqdata.T, handler *apiHandler, service *config.Service, w http.ResponseWriter, r *http.Request) {
// 5. pass execution to the handler // build context with builtin data
ctx := api.Context{Res: w, Req: r} c := r.Context()
var outData, outErr = handler.dyn.Handle(ctx, input.Data) c = context.WithValue(c, ctx.Request, r)
c = context.WithValue(c, ctx.Response, w)
c = context.WithValue(c, ctx.Auth, w)
apictx := &api.Context{Context: c}
// 6. build res from returned data // pass execution to the handler
var outData, outErr = handler.dyn.Handle(apictx, input.Data)
// build response from returned arguments
var res = api.EmptyResponse().WithError(outErr) var res = api.EmptyResponse().WithError(outErr)
for key, value := range outData { for key, value := range outData {

View File

@ -82,9 +82,9 @@ func TestWith(t *testing.T) {
t.Fatalf("setup: unexpected error <%v>", err) t.Fatalf("setup: unexpected error <%v>", err)
} }
pathHandler := func(ctx api.Context) (*struct{}, api.Err) { pathHandler := func(ctx *api.Context) (*struct{}, api.Err) {
// write value from middlewares into response // write value from middlewares into response
value := ctx.Req.Context().Value(key) value := ctx.Value(key)
if value == nil { if value == nil {
t.Fatalf("nothing found in context") t.Fatalf("nothing found in context")
} }
@ -93,7 +93,7 @@ func TestWith(t *testing.T) {
t.Fatalf("cannot cast context data to int") t.Fatalf("cannot cast context data to int")
} }
// write to response // write to response
ctx.Res.Write([]byte(fmt.Sprintf("#%d#", cast))) ctx.ResponseWriter().Write([]byte(fmt.Sprintf("#%d#", cast)))
return nil, api.ErrSuccess return nil, api.ErrSuccess
} }
@ -237,7 +237,7 @@ func TestWithAuth(t *testing.T) {
t.Fatalf("setup: unexpected error <%v>", err) t.Fatalf("setup: unexpected error <%v>", err)
} }
pathHandler := func(ctx api.Context) (*struct{}, api.Err) { pathHandler := func(ctx *api.Context) (*struct{}, api.Err) {
return nil, api.ErrNotImplemented return nil, api.ErrNotImplemented
} }
@ -290,7 +290,7 @@ func TestDynamicScope(t *testing.T) {
} }
]`, ]`,
path: "/path/{id}", path: "/path/{id}",
handler: func(struct{ Input1 uint }) (*struct{}, api.Err) { return nil, api.ErrSuccess }, handler: func(*api.Context, struct{ Input1 uint }) (*struct{}, api.Err) { return nil, api.ErrSuccess },
url: "/path/123", url: "/path/123",
body: ``, body: ``,
permissions: []string{"user[123]"}, permissions: []string{"user[123]"},
@ -311,7 +311,7 @@ func TestDynamicScope(t *testing.T) {
} }
]`, ]`,
path: "/path/{id}", path: "/path/{id}",
handler: func(struct{ Input1 uint }) (*struct{}, api.Err) { return nil, api.ErrSuccess }, handler: func(*api.Context, struct{ Input1 uint }) (*struct{}, api.Err) { return nil, api.ErrSuccess },
url: "/path/666", url: "/path/666",
body: ``, body: ``,
permissions: []string{"user[123]"}, permissions: []string{"user[123]"},
@ -332,7 +332,7 @@ func TestDynamicScope(t *testing.T) {
} }
]`, ]`,
path: "/path/{id}", path: "/path/{id}",
handler: func(struct{ User uint }) (*struct{}, api.Err) { return nil, api.ErrSuccess }, handler: func(*api.Context, struct{ User uint }) (*struct{}, api.Err) { return nil, api.ErrSuccess },
url: "/path/123", url: "/path/123",
body: ``, body: ``,
permissions: []string{"prefix.user[123].suffix"}, permissions: []string{"prefix.user[123].suffix"},
@ -354,7 +354,7 @@ func TestDynamicScope(t *testing.T) {
} }
]`, ]`,
path: "/prefix/{pid}/user/{uid}", path: "/prefix/{pid}/user/{uid}",
handler: func(struct { handler: func(*api.Context, struct {
Prefix uint Prefix uint
User uint User uint
}) (*struct{}, api.Err) { }) (*struct{}, api.Err) {
@ -381,7 +381,7 @@ func TestDynamicScope(t *testing.T) {
} }
]`, ]`,
path: "/prefix/{pid}/user/{uid}", path: "/prefix/{pid}/user/{uid}",
handler: func(struct { handler: func(*api.Context, struct {
Prefix uint Prefix uint
User uint User uint
}) (*struct{}, api.Err) { }) (*struct{}, api.Err) {
@ -409,7 +409,7 @@ func TestDynamicScope(t *testing.T) {
} }
]`, ]`,
path: "/prefix/{pid}/user/{uid}/suffix/{sid}", path: "/prefix/{pid}/user/{uid}/suffix/{sid}",
handler: func(struct { handler: func(*api.Context, struct {
Prefix uint Prefix uint
User uint User uint
Suffix uint Suffix uint
@ -438,7 +438,7 @@ func TestDynamicScope(t *testing.T) {
} }
]`, ]`,
path: "/prefix/{pid}/user/{uid}/suffix/{sid}", path: "/prefix/{pid}/user/{uid}/suffix/{sid}",
handler: func(struct { handler: func(*api.Context, struct {
Prefix uint Prefix uint
User uint User uint
Suffix uint Suffix uint

View File

@ -14,16 +14,19 @@ const errHandlerNotFunc = cerr("handler must be a func")
const errNoServiceForHandler = cerr("no service found for this handler") const errNoServiceForHandler = cerr("no service found for this handler")
// errMissingHandlerArgumentParam - missing params arguments for handler // errMissingHandlerArgumentParam - missing params arguments for handler
const errMissingHandlerArgumentParam = cerr("missing handler argument : parameter struct") const errMissingHandlerContextArgument = cerr("missing handler first argument of type *api.Context")
// errMissingHandlerInputArgument - missing params arguments for handler
const errMissingHandlerInputArgument = cerr("missing handler argument: input struct")
// errUnexpectedInput - input argument is not expected // errUnexpectedInput - input argument is not expected
const errUnexpectedInput = cerr("unexpected input struct") const errUnexpectedInput = cerr("unexpected input struct")
// errMissingHandlerOutput - missing output for handler // errMissingHandlerOutputArgument - missing output for handler
const errMissingHandlerOutput = cerr("handler must have at least 1 output") const errMissingHandlerOutputArgument = cerr("missing handler first output argument: output struct")
// errMissingHandlerOutputError - missing error output for handler // errMissingHandlerOutputError - missing error output for handler
const errMissingHandlerOutputError = cerr("handler must have its last output of type api.Err") const errMissingHandlerOutputError = cerr("missing handler last output argument of type api.Err")
// errMissingRequestArgument - missing request argument for handler // errMissingRequestArgument - missing request argument for handler
const errMissingRequestArgument = cerr("handler first argument must be of type api.Request") const errMissingRequestArgument = cerr("handler first argument must be of type api.Request")
@ -34,17 +37,14 @@ const errMissingParamArgument = cerr("handler second argument must be a struct")
// errUnexportedName - argument is unexported in struct // errUnexportedName - argument is unexported in struct
const errUnexportedName = cerr("unexported name") const errUnexportedName = cerr("unexported name")
// errMissingParamOutput - missing output argument for handler // errWrongOutputArgumentType - wrong type for output first argument
const errMissingParamOutput = cerr("handler first output must be a *struct") const errWrongOutputArgumentType = cerr("handler first output argument must be a *struct")
// errMissingParamFromConfig - missing a parameter in handler struct // errMissingConfigArgument - missing an input/output argument in handler struct
const errMissingParamFromConfig = cerr("missing a parameter from configuration") const errMissingConfigArgument = cerr("missing an argument from the configuration")
// errMissingOutputFromConfig - missing a parameter in handler struct
const errMissingOutputFromConfig = cerr("missing a parameter from configuration")
// errWrongParamTypeFromConfig - a configuration parameter type is invalid in the handler param struct // errWrongParamTypeFromConfig - a configuration parameter type is invalid in the handler param struct
const errWrongParamTypeFromConfig = cerr("invalid struct field type") const errWrongParamTypeFromConfig = cerr("invalid struct field type")
// errMissingHandlerErrorOutput - missing handler output error // errMissingHandlerErrorArgument - missing handler output error
const errMissingHandlerErrorOutput = cerr("last output must be of type api.Err") const errMissingHandlerErrorArgument = cerr("last output must be of type api.Err")

View File

@ -9,73 +9,69 @@ import (
"git.xdrm.io/go/aicra/internal/config" "git.xdrm.io/go/aicra/internal/config"
) )
// Handler represents a dynamic api handler // Handler represents a dynamic aicra service handler
type Handler struct { type Handler struct {
spec *signature // signature defined from the service configuration
signature *Signature
// fn provided function that will be the service's handler
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 dynamic function and checks its signature against a
// // service configuration
// @fn must have as a signature : `func(inputStruct) (*outputStruct, api.Err)` //e
// - `inputStruct` is a struct{} containing a field for each service input (with valid reflect.Type) // `fn` must have as a signature : `func(*api.Context, in) (*out, api.Err)`
// - `outputStruct` is a struct{} containing a field for each service output (with valid reflect.Type) // - `in` is a struct{} containing a field for each service input (with valid reflect.Type)
// - `out` 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, `in` MUST be omitted
// - it there is no input, `inputStruct` must be omitted // - it there is no output, `out` 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{ var (
spec: signatureFromService(service), h = &Handler{
signature: BuildSignature(service),
fn: fn, fn: fn,
} }
fnType = reflect.TypeOf(fn)
)
impl := reflect.TypeOf(fn) if fnType.Kind() != reflect.Func {
if impl.Kind() != reflect.Func {
return nil, errHandlerNotFunc return nil, errHandlerNotFunc
} }
if err := h.signature.ValidateInput(fnType); err != nil {
h.hasContext = impl.NumIn() >= 1 && reflect.TypeOf(api.Context{}).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(impl); err != nil { if err := h.signature.ValidateOutput(fnType); err != nil {
return nil, fmt.Errorf("output: %w", err) return nil, fmt.Errorf("output: %w", err)
} }
return h, nil return h, nil
} }
// Handle binds input @data into the dynamic function and returns map output // Handle binds input `data` into the dynamic function and returns an output map
func (h *Handler) Handle(ctx api.Context, data map[string]interface{}) (map[string]interface{}, api.Err) { func (h *Handler) Handle(ctx *api.Context, data map[string]interface{}) (map[string]interface{}, api.Err) {
var ert = reflect.TypeOf(api.Err{}) var (
var fnv = reflect.ValueOf(h.fn) ert = reflect.TypeOf(api.Err{})
fnv = reflect.ValueOf(h.fn)
callArgs = make([]reflect.Value, 0)
)
callArgs := []reflect.Value{} // bind context
// bind context if used in handler
if h.hasContext {
callArgs = append(callArgs, reflect.ValueOf(ctx)) callArgs = append(callArgs, reflect.ValueOf(ctx))
}
// bind input data inputStructRequired := fnv.Type().NumIn() > 1
if fnv.Type().NumIn() > h.dataIndex {
// bind input arguments
if inputStructRequired {
// create zero value struct // create zero value struct
callStructPtr := reflect.New(fnv.Type().In(0)) var (
callStruct := callStructPtr.Elem() callStructPtr = reflect.New(fnv.Type().In(1))
callStruct = callStructPtr.Elem()
)
// set each field // set each field
for name := range h.spec.Input { for name := range h.signature.Input {
field := callStruct.FieldByName(name) field := callStruct.FieldByName(name)
if !field.CanSet() { if !field.CanSet() {
continue continue
@ -115,12 +111,12 @@ func (h *Handler) Handle(ctx api.Context, data map[string]interface{}) (map[stri
callArgs = append(callArgs, callStruct) callArgs = append(callArgs, callStruct)
} }
// call the HandlerFn // call the handler
output := fnv.Call(callArgs) output := fnv.Call(callArgs)
// no output OR pointer to output struct is nil // no output OR pointer to output struct is nil
outdata := make(map[string]interface{}) outdata := make(map[string]interface{})
if len(h.spec.Output) < 1 || output[0].IsNil() { if len(h.signature.Output) < 1 || output[0].IsNil() {
var structerr = output[len(output)-1].Convert(ert) var structerr = output[len(output)-1].Convert(ert)
return outdata, api.Err{ return outdata, api.Err{
Code: int(structerr.FieldByName("Code").Int()), Code: int(structerr.FieldByName("Code").Int()),
@ -132,7 +128,7 @@ func (h *Handler) Handle(ctx api.Context, data map[string]interface{}) (map[stri
// extract struct from pointer // extract struct from pointer
returnStruct := output[0].Elem() returnStruct := output[0].Elem()
for name := range h.spec.Output { for name := range h.signature.Output {
field := returnStruct.FieldByName(name) field := returnStruct.FieldByName(name)
outdata[name] = field.Interface() outdata[name] = field.Interface()
} }

View File

@ -8,7 +8,7 @@ import (
"git.xdrm.io/go/aicra/api" "git.xdrm.io/go/aicra/api"
) )
type testsignature signature type testsignature Signature
// builds a mock service with provided arguments as Input and matched as Output // builds a mock service with provided arguments as Input and matched as Output
func (s *testsignature) withArgs(dtypes ...reflect.Type) *testsignature { func (s *testsignature) withArgs(dtypes ...reflect.Type) *testsignature {
@ -52,7 +52,7 @@ func TestInput(t *testing.T) {
{ {
Name: "none required none provided", Name: "none required none provided",
Spec: (&testsignature{}).withArgs(), Spec: (&testsignature{}).withArgs(),
Fn: func() (*struct{}, api.Err) { return nil, api.ErrSuccess }, Fn: func(*api.Context) (*struct{}, api.Err) { return nil, api.ErrSuccess },
HasContext: false, HasContext: false,
Input: []interface{}{}, Input: []interface{}{},
ExpectedOutput: []interface{}{}, ExpectedOutput: []interface{}{},
@ -61,7 +61,7 @@ func TestInput(t *testing.T) {
{ {
Name: "int proxy (0)", Name: "int proxy (0)",
Spec: (&testsignature{}).withArgs(reflect.TypeOf(int(0))), Spec: (&testsignature{}).withArgs(reflect.TypeOf(int(0))),
Fn: func(in intstruct) (*intstruct, api.Err) { Fn: func(ctx *api.Context, in intstruct) (*intstruct, api.Err) {
return &intstruct{P1: in.P1}, api.ErrSuccess return &intstruct{P1: in.P1}, api.ErrSuccess
}, },
HasContext: false, HasContext: false,
@ -72,7 +72,7 @@ func TestInput(t *testing.T) {
{ {
Name: "int proxy (11)", Name: "int proxy (11)",
Spec: (&testsignature{}).withArgs(reflect.TypeOf(int(0))), Spec: (&testsignature{}).withArgs(reflect.TypeOf(int(0))),
Fn: func(in intstruct) (*intstruct, api.Err) { Fn: func(ctx *api.Context, in intstruct) (*intstruct, api.Err) {
return &intstruct{P1: in.P1}, api.ErrSuccess return &intstruct{P1: in.P1}, api.ErrSuccess
}, },
HasContext: false, HasContext: false,
@ -83,7 +83,7 @@ func TestInput(t *testing.T) {
{ {
Name: "*int proxy (nil)", Name: "*int proxy (nil)",
Spec: (&testsignature{}).withArgs(reflect.TypeOf(new(int))), Spec: (&testsignature{}).withArgs(reflect.TypeOf(new(int))),
Fn: func(in intptrstruct) (*intptrstruct, api.Err) { Fn: func(ctx *api.Context, in intptrstruct) (*intptrstruct, api.Err) {
return &intptrstruct{P1: in.P1}, api.ErrSuccess return &intptrstruct{P1: in.P1}, api.ErrSuccess
}, },
HasContext: false, HasContext: false,
@ -94,7 +94,7 @@ func TestInput(t *testing.T) {
{ {
Name: "*int proxy (28)", Name: "*int proxy (28)",
Spec: (&testsignature{}).withArgs(reflect.TypeOf(new(int))), Spec: (&testsignature{}).withArgs(reflect.TypeOf(new(int))),
Fn: func(in intptrstruct) (*intstruct, api.Err) { Fn: func(ctx *api.Context, in intptrstruct) (*intstruct, api.Err) {
return &intstruct{P1: *in.P1}, api.ErrSuccess return &intstruct{P1: *in.P1}, api.ErrSuccess
}, },
HasContext: false, HasContext: false,
@ -105,7 +105,7 @@ func TestInput(t *testing.T) {
{ {
Name: "*int proxy (13)", Name: "*int proxy (13)",
Spec: (&testsignature{}).withArgs(reflect.TypeOf(new(int))), Spec: (&testsignature{}).withArgs(reflect.TypeOf(new(int))),
Fn: func(in intptrstruct) (*intstruct, api.Err) { Fn: func(ctx *api.Context, in intptrstruct) (*intstruct, api.Err) {
return &intstruct{P1: *in.P1}, api.ErrSuccess return &intstruct{P1: *in.P1}, api.ErrSuccess
}, },
HasContext: false, HasContext: false,
@ -119,16 +119,9 @@ func TestInput(t *testing.T) {
t.Run(tcase.Name, func(t *testing.T) { t.Run(tcase.Name, func(t *testing.T) {
t.Parallel() t.Parallel()
var dataIndex = 0
if tcase.HasContext {
dataIndex = 1
}
var handler = &Handler{ var handler = &Handler{
spec: &signature{Input: tcase.Spec.Input, Output: tcase.Spec.Output}, signature: &Signature{Input: tcase.Spec.Input, Output: tcase.Spec.Output},
fn: tcase.Fn, fn: tcase.Fn,
dataIndex: dataIndex,
hasContext: tcase.HasContext,
} }
// build input // build input
@ -138,7 +131,7 @@ func TestInput(t *testing.T) {
input[key] = val input[key] = val
} }
var output, err = handler.Handle(api.Context{}, input) var output, err = handler.Handle(&api.Context{}, input)
if err != tcase.ExpectedErr { if err != tcase.ExpectedErr {
t.Fatalf("expected api error <%v> got <%v>", tcase.ExpectedErr, err) t.Fatalf("expected api error <%v> got <%v>", tcase.ExpectedErr, err)
} }

View File

@ -9,15 +9,17 @@ import (
"git.xdrm.io/go/aicra/internal/config" "git.xdrm.io/go/aicra/internal/config"
) )
// signature represents input/output arguments for a dynamic function // Signature represents input/output arguments for service from the aicra configuration
type signature struct { type Signature struct {
// Input arguments of the service
Input map[string]reflect.Type Input map[string]reflect.Type
// Output arguments of the service
Output map[string]reflect.Type Output map[string]reflect.Type
} }
// builds a spec from the configuration service // BuildSignature builds a signature for a service configuration
func signatureFromService(service config.Service) *signature { func BuildSignature(service config.Service) *Signature {
s := &signature{ 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),
} }
@ -44,31 +46,32 @@ func signatureFromService(service config.Service) *signature {
return s return s
} }
// checks for HandlerFn input arguments // ValidateInput validates a handler's input arguments against the service signature
func (s *signature) checkInput(impl reflect.Type, index int) error { func (s *Signature) ValidateInput(handlerType reflect.Type) error {
var requiredInput, structIndex = index, index ctxType := reflect.TypeOf(api.Context{})
if len(s.Input) > 0 { // arguments struct
requiredInput++ // missing or invalid first arg: api.Context
if handlerType.NumIn() < 1 || ctxType.AssignableTo(handlerType.In(0)) {
return errMissingHandlerContextArgument
} }
// missing arguments // no input required
if impl.NumIn() > requiredInput { if len(s.Input) == 0 {
// input struct provided
if handlerType.NumIn() > 1 {
return errUnexpectedInput return errUnexpectedInput
} }
// none required
if len(s.Input) == 0 {
return nil return nil
} }
// too much arguments // too much arguments
if impl.NumIn() != requiredInput { if handlerType.NumIn() > 2 {
return errMissingHandlerArgumentParam return errMissingHandlerInputArgument
} }
// arg must be a struct // arg must be a struct
structArg := impl.In(structIndex) inStruct := handlerType.In(1)
if structArg.Kind() != reflect.Struct { if inStruct.Kind() != reflect.Struct {
return errMissingParamArgument return errMissingParamArgument
} }
@ -78,9 +81,9 @@ func (s *signature) checkInput(impl reflect.Type, index int) error {
return fmt.Errorf("%s: %w", name, errUnexportedName) return fmt.Errorf("%s: %w", name, errUnexportedName)
} }
field, exists := structArg.FieldByName(name) field, exists := inStruct.FieldByName(name)
if !exists { if !exists {
return fmt.Errorf("%s: %w", name, errMissingParamFromConfig) return fmt.Errorf("%s: %w", name, errMissingConfigArgument)
} }
if !ptype.AssignableTo(field.Type) { if !ptype.AssignableTo(field.Type) {
@ -91,16 +94,18 @@ func (s *signature) checkInput(impl reflect.Type, index int) error {
return nil return nil
} }
// checks for HandlerFn output arguments // ValidateOutput validates a handler's output arguments against the service signature
func (s signature) checkOutput(impl reflect.Type) error { func (s Signature) ValidateOutput(handlerType reflect.Type) error {
if impl.NumOut() < 1 { errType := reflect.TypeOf(api.ErrUnknown)
return errMissingHandlerOutput
if handlerType.NumOut() < 1 {
return errMissingHandlerErrorArgument
} }
// last output must be api.Err // last output must be api.Err
errOutput := impl.Out(impl.NumOut() - 1) lastArgType := handlerType.Out(handlerType.NumOut() - 1)
if !errOutput.AssignableTo(reflect.TypeOf(api.ErrUnknown)) { if !lastArgType.AssignableTo(errType) {
return errMissingHandlerErrorOutput return errMissingHandlerErrorArgument
} }
// no output -> ok // no output -> ok
@ -108,19 +113,19 @@ func (s signature) checkOutput(impl reflect.Type) error {
return nil return nil
} }
if impl.NumOut() != 2 { if handlerType.NumOut() < 2 {
return errMissingParamOutput return errMissingHandlerOutputArgument
} }
// fail if first output is not a pointer to struct // fail if first output is not a pointer to struct
structOutputPtr := impl.Out(0) outStructPtr := handlerType.Out(0)
if structOutputPtr.Kind() != reflect.Ptr { if outStructPtr.Kind() != reflect.Ptr {
return errMissingParamOutput return errWrongOutputArgumentType
} }
structOutput := structOutputPtr.Elem() outStruct := outStructPtr.Elem()
if structOutput.Kind() != reflect.Struct { if outStruct.Kind() != reflect.Struct {
return errMissingParamOutput return errWrongOutputArgumentType
} }
// fail on invalid output // fail on invalid output
@ -129,9 +134,9 @@ func (s signature) checkOutput(impl reflect.Type) error {
return fmt.Errorf("%s: %w", name, errUnexportedName) return fmt.Errorf("%s: %w", name, errUnexportedName)
} }
field, exists := structOutput.FieldByName(name) field, exists := outStruct.FieldByName(name)
if !exists { if !exists {
return fmt.Errorf("%s: %w", name, errMissingOutputFromConfig) return fmt.Errorf("%s: %w", name, errMissingConfigArgument)
} }
// ignore types evalutating to nil // ignore types evalutating to nil

View File

@ -20,22 +20,22 @@ func TestInputCheck(t *testing.T) {
{ {
Name: "no input 0 given", Name: "no input 0 given",
Input: map[string]reflect.Type{}, Input: map[string]reflect.Type{},
Fn: func() {}, Fn: func(*api.Context) {},
FnCtx: func(api.Context) {}, FnCtx: func(*api.Context) {},
Err: nil, Err: nil,
}, },
{ {
Name: "no input 1 given", Name: "no input 1 given",
Input: map[string]reflect.Type{}, Input: map[string]reflect.Type{},
Fn: func(int) {}, Fn: func(*api.Context, int) {},
FnCtx: func(api.Context, int) {}, FnCtx: func(*api.Context, int) {},
Err: errUnexpectedInput, Err: errUnexpectedInput,
}, },
{ {
Name: "no input 2 given", Name: "no input 2 given",
Input: map[string]reflect.Type{}, Input: map[string]reflect.Type{},
Fn: func(int, string) {}, Fn: func(*api.Context, int, string) {},
FnCtx: func(api.Context, int, string) {}, FnCtx: func(*api.Context, int, string) {},
Err: errUnexpectedInput, Err: errUnexpectedInput,
}, },
{ {
@ -43,17 +43,17 @@ func TestInputCheck(t *testing.T) {
Input: map[string]reflect.Type{ Input: map[string]reflect.Type{
"Test1": reflect.TypeOf(int(0)), "Test1": reflect.TypeOf(int(0)),
}, },
Fn: func() {}, Fn: func(*api.Context) {},
FnCtx: func(api.Context) {}, FnCtx: func(*api.Context) {},
Err: errMissingHandlerArgumentParam, Err: errMissingHandlerInputArgument,
}, },
{ {
Name: "1 input non-struct given", Name: "1 input non-struct given",
Input: map[string]reflect.Type{ Input: map[string]reflect.Type{
"Test1": reflect.TypeOf(int(0)), "Test1": reflect.TypeOf(int(0)),
}, },
Fn: func(int) {}, Fn: func(*api.Context, int) {},
FnCtx: func(api.Context, int) {}, FnCtx: func(*api.Context, int) {},
Err: errMissingParamArgument, Err: errMissingParamArgument,
}, },
{ {
@ -61,8 +61,8 @@ func TestInputCheck(t *testing.T) {
Input: map[string]reflect.Type{ Input: map[string]reflect.Type{
"test1": reflect.TypeOf(int(0)), "test1": reflect.TypeOf(int(0)),
}, },
Fn: func(struct{}) {}, Fn: func(*api.Context, struct{}) {},
FnCtx: func(api.Context, struct{}) {}, FnCtx: func(*api.Context, struct{}) {},
Err: errUnexportedName, Err: errUnexportedName,
}, },
{ {
@ -70,17 +70,17 @@ func TestInputCheck(t *testing.T) {
Input: map[string]reflect.Type{ Input: map[string]reflect.Type{
"Test1": reflect.TypeOf(int(0)), "Test1": reflect.TypeOf(int(0)),
}, },
Fn: func(struct{}) {}, Fn: func(*api.Context, struct{}) {},
FnCtx: func(api.Context, struct{}) {}, FnCtx: func(*api.Context, struct{}) {},
Err: errMissingParamFromConfig, Err: errMissingConfigArgument,
}, },
{ {
Name: "1 input invalid given", Name: "1 input invalid given",
Input: map[string]reflect.Type{ Input: map[string]reflect.Type{
"Test1": reflect.TypeOf(int(0)), "Test1": reflect.TypeOf(int(0)),
}, },
Fn: func(struct{ Test1 string }) {}, Fn: func(*api.Context, struct{ Test1 string }) {},
FnCtx: func(api.Context, struct{ Test1 string }) {}, FnCtx: func(*api.Context, struct{ Test1 string }) {},
Err: errWrongParamTypeFromConfig, Err: errWrongParamTypeFromConfig,
}, },
{ {
@ -88,8 +88,8 @@ func TestInputCheck(t *testing.T) {
Input: map[string]reflect.Type{ Input: map[string]reflect.Type{
"Test1": reflect.TypeOf(int(0)), "Test1": reflect.TypeOf(int(0)),
}, },
Fn: func(struct{ Test1 int }) {}, Fn: func(*api.Context, struct{ Test1 int }) {},
FnCtx: func(api.Context, struct{ Test1 int }) {}, FnCtx: func(*api.Context, struct{ Test1 int }) {},
Err: nil, Err: nil,
}, },
{ {
@ -97,17 +97,17 @@ func TestInputCheck(t *testing.T) {
Input: map[string]reflect.Type{ Input: map[string]reflect.Type{
"Test1": reflect.TypeOf(new(int)), "Test1": reflect.TypeOf(new(int)),
}, },
Fn: func(struct{}) {}, Fn: func(*api.Context, struct{}) {},
FnCtx: func(api.Context, struct{}) {}, FnCtx: func(*api.Context, struct{}) {},
Err: errMissingParamFromConfig, Err: errMissingConfigArgument,
}, },
{ {
Name: "1 input ptr invalid given", Name: "1 input ptr invalid given",
Input: map[string]reflect.Type{ Input: map[string]reflect.Type{
"Test1": reflect.TypeOf(new(int)), "Test1": reflect.TypeOf(new(int)),
}, },
Fn: func(struct{ Test1 string }) {}, Fn: func(*api.Context, struct{ Test1 string }) {},
FnCtx: func(api.Context, struct{ Test1 string }) {}, FnCtx: func(*api.Context, struct{ Test1 string }) {},
Err: errWrongParamTypeFromConfig, Err: errWrongParamTypeFromConfig,
}, },
{ {
@ -115,8 +115,8 @@ func TestInputCheck(t *testing.T) {
Input: map[string]reflect.Type{ Input: map[string]reflect.Type{
"Test1": reflect.TypeOf(new(int)), "Test1": reflect.TypeOf(new(int)),
}, },
Fn: func(struct{ Test1 *string }) {}, Fn: func(*api.Context, struct{ Test1 *string }) {},
FnCtx: func(api.Context, struct{ Test1 *string }) {}, FnCtx: func(*api.Context, struct{ Test1 *string }) {},
Err: errWrongParamTypeFromConfig, Err: errWrongParamTypeFromConfig,
}, },
{ {
@ -124,8 +124,8 @@ func TestInputCheck(t *testing.T) {
Input: map[string]reflect.Type{ Input: map[string]reflect.Type{
"Test1": reflect.TypeOf(new(int)), "Test1": reflect.TypeOf(new(int)),
}, },
Fn: func(struct{ Test1 *int }) {}, Fn: func(*api.Context, struct{ Test1 *int }) {},
FnCtx: func(api.Context, struct{ Test1 *int }) {}, FnCtx: func(*api.Context, struct{ Test1 *int }) {},
Err: nil, Err: nil,
}, },
{ {
@ -133,8 +133,8 @@ func TestInputCheck(t *testing.T) {
Input: map[string]reflect.Type{ Input: map[string]reflect.Type{
"Test1": reflect.TypeOf(string("")), "Test1": reflect.TypeOf(string("")),
}, },
Fn: func(struct{ Test1 string }) {}, Fn: func(*api.Context, struct{ Test1 string }) {},
FnCtx: func(api.Context, struct{ Test1 string }) {}, FnCtx: func(*api.Context, struct{ Test1 string }) {},
Err: nil, Err: nil,
}, },
{ {
@ -142,8 +142,8 @@ func TestInputCheck(t *testing.T) {
Input: map[string]reflect.Type{ Input: map[string]reflect.Type{
"Test1": reflect.TypeOf(uint(0)), "Test1": reflect.TypeOf(uint(0)),
}, },
Fn: func(struct{ Test1 uint }) {}, Fn: func(*api.Context, struct{ Test1 uint }) {},
FnCtx: func(api.Context, struct{ Test1 uint }) {}, FnCtx: func(*api.Context, struct{ Test1 uint }) {},
Err: nil, Err: nil,
}, },
{ {
@ -151,8 +151,8 @@ func TestInputCheck(t *testing.T) {
Input: map[string]reflect.Type{ Input: map[string]reflect.Type{
"Test1": reflect.TypeOf(float64(0)), "Test1": reflect.TypeOf(float64(0)),
}, },
Fn: func(struct{ Test1 float64 }) {}, Fn: func(*api.Context, struct{ Test1 float64 }) {},
FnCtx: func(api.Context, struct{ Test1 float64 }) {}, FnCtx: func(*api.Context, struct{ Test1 float64 }) {},
Err: nil, Err: nil,
}, },
{ {
@ -160,8 +160,8 @@ func TestInputCheck(t *testing.T) {
Input: map[string]reflect.Type{ Input: map[string]reflect.Type{
"Test1": reflect.TypeOf([]byte("")), "Test1": reflect.TypeOf([]byte("")),
}, },
Fn: func(struct{ Test1 []byte }) {}, Fn: func(*api.Context, struct{ Test1 []byte }) {},
FnCtx: func(api.Context, struct{ Test1 []byte }) {}, FnCtx: func(*api.Context, struct{ Test1 []byte }) {},
Err: nil, Err: nil,
}, },
{ {
@ -169,8 +169,8 @@ func TestInputCheck(t *testing.T) {
Input: map[string]reflect.Type{ Input: map[string]reflect.Type{
"Test1": reflect.TypeOf([]rune("")), "Test1": reflect.TypeOf([]rune("")),
}, },
Fn: func(struct{ Test1 []rune }) {}, Fn: func(*api.Context, struct{ Test1 []rune }) {},
FnCtx: func(api.Context, struct{ Test1 []rune }) {}, FnCtx: func(*api.Context, struct{ Test1 []rune }) {},
Err: nil, Err: nil,
}, },
{ {
@ -178,8 +178,8 @@ func TestInputCheck(t *testing.T) {
Input: map[string]reflect.Type{ Input: map[string]reflect.Type{
"Test1": reflect.TypeOf(new(string)), "Test1": reflect.TypeOf(new(string)),
}, },
Fn: func(struct{ Test1 *string }) {}, Fn: func(*api.Context, struct{ Test1 *string }) {},
FnCtx: func(api.Context, struct{ Test1 *string }) {}, FnCtx: func(*api.Context, struct{ Test1 *string }) {},
Err: nil, Err: nil,
}, },
{ {
@ -187,8 +187,8 @@ func TestInputCheck(t *testing.T) {
Input: map[string]reflect.Type{ Input: map[string]reflect.Type{
"Test1": reflect.TypeOf(new(uint)), "Test1": reflect.TypeOf(new(uint)),
}, },
Fn: func(struct{ Test1 *uint }) {}, Fn: func(*api.Context, struct{ Test1 *uint }) {},
FnCtx: func(api.Context, struct{ Test1 *uint }) {}, FnCtx: func(*api.Context, struct{ Test1 *uint }) {},
Err: nil, Err: nil,
}, },
{ {
@ -196,8 +196,8 @@ func TestInputCheck(t *testing.T) {
Input: map[string]reflect.Type{ Input: map[string]reflect.Type{
"Test1": reflect.TypeOf(new(float64)), "Test1": reflect.TypeOf(new(float64)),
}, },
Fn: func(struct{ Test1 *float64 }) {}, Fn: func(*api.Context, struct{ Test1 *float64 }) {},
FnCtx: func(api.Context, struct{ Test1 *float64 }) {}, FnCtx: func(*api.Context, struct{ Test1 *float64 }) {},
Err: nil, Err: nil,
}, },
{ {
@ -205,8 +205,8 @@ func TestInputCheck(t *testing.T) {
Input: map[string]reflect.Type{ Input: map[string]reflect.Type{
"Test1": reflect.TypeOf(new([]byte)), "Test1": reflect.TypeOf(new([]byte)),
}, },
Fn: func(struct{ Test1 *[]byte }) {}, Fn: func(*api.Context, struct{ Test1 *[]byte }) {},
FnCtx: func(api.Context, struct{ Test1 *[]byte }) {}, FnCtx: func(*api.Context, struct{ Test1 *[]byte }) {},
Err: nil, Err: nil,
}, },
{ {
@ -214,8 +214,8 @@ func TestInputCheck(t *testing.T) {
Input: map[string]reflect.Type{ Input: map[string]reflect.Type{
"Test1": reflect.TypeOf(new([]rune)), "Test1": reflect.TypeOf(new([]rune)),
}, },
Fn: func(struct{ Test1 *[]rune }) {}, Fn: func(*api.Context, struct{ Test1 *[]rune }) {},
FnCtx: func(api.Context, struct{ Test1 *[]rune }) {}, FnCtx: func(*api.Context, struct{ Test1 *[]rune }) {},
Err: nil, Err: nil,
}, },
} }
@ -225,13 +225,12 @@ func TestInputCheck(t *testing.T) {
t.Parallel() t.Parallel()
// mock spec // mock spec
s := signature{ s := Signature{
Input: tcase.Input, Input: tcase.Input,
Output: nil, Output: nil,
} }
t.Run("with-context", func(t *testing.T) { err := s.ValidateInput(reflect.TypeOf(tcase.FnCtx))
err := s.checkInput(reflect.TypeOf(tcase.FnCtx), 1)
if err == nil && tcase.Err != nil { if err == nil && tcase.Err != nil {
t.Errorf("expected an error: '%s'", tcase.Err.Error()) t.Errorf("expected an error: '%s'", tcase.Err.Error())
t.FailNow() t.FailNow()
@ -248,25 +247,6 @@ func TestInputCheck(t *testing.T) {
} }
} }
}) })
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()
}
}
})
})
} }
} }
@ -279,25 +259,37 @@ func TestOutputCheck(t *testing.T) {
// no input -> missing api.Err // no input -> missing api.Err
{ {
Output: map[string]reflect.Type{}, Output: map[string]reflect.Type{},
Fn: func() {}, Fn: func(*api.Context) {},
Err: errMissingHandlerOutput, Err: errMissingHandlerOutputArgument,
}, },
// no input -> with last type not api.Err // no input -> with last type not api.Err
{ {
Output: map[string]reflect.Type{}, Output: map[string]reflect.Type{},
Fn: func() bool { return true }, Fn: func(*api.Context) bool { return true },
Err: errMissingHandlerErrorOutput, Err: errMissingHandlerErrorArgument,
}, },
// no input -> with api.Err // no input -> with api.Err
{ {
Output: map[string]reflect.Type{}, Output: map[string]reflect.Type{},
Fn: func() api.Err { return api.ErrSuccess }, Fn: func(*api.Context) api.Err { return api.ErrSuccess },
Err: nil, Err: nil,
}, },
// no input -> missing *api.Context
{
Output: map[string]reflect.Type{},
Fn: func(*api.Context) api.Err { return api.ErrSuccess },
Err: errMissingHandlerContextArgument,
},
// no input -> invlaid *api.Context type
{
Output: map[string]reflect.Type{},
Fn: func(*api.Context, int) api.Err { return api.ErrSuccess },
Err: errMissingHandlerContextArgument,
},
// func can have output if not specified // func can have output if not specified
{ {
Output: map[string]reflect.Type{}, Output: map[string]reflect.Type{},
Fn: func() (*struct{}, api.Err) { return nil, api.ErrSuccess }, Fn: func(*api.Context) (*struct{}, api.Err) { return nil, api.ErrSuccess },
Err: nil, Err: nil,
}, },
// missing output struct in func // missing output struct in func
@ -306,7 +298,7 @@ func TestOutputCheck(t *testing.T) {
"Test1": reflect.TypeOf(int(0)), "Test1": reflect.TypeOf(int(0)),
}, },
Fn: func() api.Err { return api.ErrSuccess }, Fn: func() api.Err { return api.ErrSuccess },
Err: errMissingParamOutput, Err: errWrongOutputArgumentType,
}, },
// output not a pointer // output not a pointer
{ {
@ -314,7 +306,7 @@ func TestOutputCheck(t *testing.T) {
"Test1": reflect.TypeOf(int(0)), "Test1": reflect.TypeOf(int(0)),
}, },
Fn: func() (int, api.Err) { return 0, api.ErrSuccess }, Fn: func() (int, api.Err) { return 0, api.ErrSuccess },
Err: errMissingParamOutput, Err: errWrongOutputArgumentType,
}, },
// output not a pointer to struct // output not a pointer to struct
{ {
@ -322,7 +314,7 @@ func TestOutputCheck(t *testing.T) {
"Test1": reflect.TypeOf(int(0)), "Test1": reflect.TypeOf(int(0)),
}, },
Fn: func() (*int, api.Err) { return nil, api.ErrSuccess }, Fn: func() (*int, api.Err) { return nil, api.ErrSuccess },
Err: errMissingParamOutput, Err: errWrongOutputArgumentType,
}, },
// unexported param name // unexported param name
{ {
@ -338,7 +330,7 @@ func TestOutputCheck(t *testing.T) {
"Test1": reflect.TypeOf(int(0)), "Test1": reflect.TypeOf(int(0)),
}, },
Fn: func() (*struct{}, api.Err) { return nil, api.ErrSuccess }, Fn: func() (*struct{}, api.Err) { return nil, api.ErrSuccess },
Err: errMissingParamFromConfig, Err: errMissingConfigArgument,
}, },
// output field invalid type // output field invalid type
{ {
@ -371,12 +363,12 @@ func TestOutputCheck(t *testing.T) {
t.Parallel() t.Parallel()
// mock spec // mock spec
s := signature{ s := Signature{
Input: nil, Input: nil,
Output: tcase.Output, Output: tcase.Output,
} }
err := s.checkOutput(reflect.TypeOf(tcase.Fn)) err := s.ValidateOutput(reflect.TypeOf(tcase.Fn))
if err == nil && tcase.Err != nil { if err == nil && tcase.Err != nil {
t.Errorf("expected an error: '%s'", tcase.Err.Error()) t.Errorf("expected an error: '%s'", tcase.Err.Error())
t.FailNow() t.FailNow()