From 081e48002fc5f02f7c95ee96cf7bbd2959088003 Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Sun, 29 Mar 2020 15:00:22 +0200 Subject: [PATCH] create dynamic package to handle reflection at runtime to check for handlers signature --- dynamic/errors.go | 51 +++++++++++++++++++ dynamic/handler.go | 44 +++++++++++++++++ dynamic/spec.go | 121 +++++++++++++++++++++++++++++++++++++++++++++ dynamic/types.go | 17 +++++++ 4 files changed, 233 insertions(+) create mode 100644 dynamic/errors.go create mode 100644 dynamic/handler.go create mode 100644 dynamic/spec.go create mode 100644 dynamic/types.go diff --git a/dynamic/errors.go b/dynamic/errors.go new file mode 100644 index 0000000..60acaec --- /dev/null +++ b/dynamic/errors.go @@ -0,0 +1,51 @@ +package dynamic + +// cerr allows you to create constant "const" error with type boxing. +type cerr string + +// Error implements the error builtin interface. +func (err cerr) Error() string { + return string(err) +} + +// ErrHandlerNotFunc - handler is not a func +const ErrHandlerNotFunc = cerr("handler must be a func") + +// ErrNoServiceForHandler - no service matching this handler +const ErrNoServiceForHandler = cerr("no service found for this handler") + +// ErrMissingHandlerArgument - missing arguments for handler +const ErrMissingHandlerArgument = cerr("handler must have at least 1 arguments") + +// ErrMissingHandlerArgumentParam - missing params arguments for handler +const ErrMissingHandlerArgumentParam = cerr("missing handler argument 2 : parameter struct") + +// ErrMissingHandlerOutput - missing output for handler +const ErrMissingHandlerOutput = cerr("handler must have at least 1 output") + +// ErrMissingHandlerOutputError - missing error output for handler +const ErrMissingHandlerOutputError = cerr("handler must have its last output of type api.Error") + +// ErrMissingRequestArgument - missing request argument for handler +const ErrMissingRequestArgument = cerr("handler first argument must be of type api.Request") + +// ErrMissingParamArgument - missing parameters argument for handler +const ErrMissingParamArgument = cerr("handler second argument must be a struct") + +// ErrMissingParamOutput - missing output argument for handler +const ErrMissingParamOutput = cerr("handler first output must be a struct") + +// ErrMissingParamFromConfig - missing a parameter in handler struct +const ErrMissingParamFromConfig = cerr("missing a parameter from 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 +const ErrWrongParamTypeFromConfig = cerr("invalid parameter struct type from configuration") + +// ErrWrongOutputTypeFromConfig - a configuration output type is invalid in the handler output struct +const ErrWrongOutputTypeFromConfig = cerr("invalid output struct type from configuration") + +// ErrMissingHandlerErrorOutput - missing handler output error +const ErrMissingHandlerErrorOutput = cerr("last output must be of type api.Error") diff --git a/dynamic/handler.go b/dynamic/handler.go new file mode 100644 index 0000000..2be0957 --- /dev/null +++ b/dynamic/handler.go @@ -0,0 +1,44 @@ +package dynamic + +import ( + "fmt" + "reflect" + + "git.xdrm.io/go/aicra/internal/config" +) + +// Build a handler from a service configuration and a HandlerFn +// +// a HandlerFn must have as a signature : `func(api.Request, inputStruct) (outputStruct, api.Error)` +// - `inputStruct` is a struct{} containing a field for each service input (with valid reflect.Type) +// - `outputStruct` is a struct{} containing a field for each service output (with valid reflect.Type) +// +// Special cases: +// - it there is no input, `inputStruct` can be omitted +// - it there is no output, `outputStruct` can be omitted +func Build(fn HandlerFn, service config.Service) (*Handler, error) { + h := &Handler{ + spec: makeSpec(service), + fn: fn, + } + + fnt := reflect.TypeOf(fn) + + if fnt.Kind() != reflect.Func { + return nil, ErrHandlerNotFunc + } + + if err := h.spec.checkInput(fnt); err != nil { + return nil, fmt.Errorf("input: %w", err) + } + if err := h.spec.checkOutput(fnt); err != nil { + return nil, fmt.Errorf("output: %w", err) + } + + return h, nil +} + +// Handle +func (h *Handler) Handle() { + +} diff --git a/dynamic/spec.go b/dynamic/spec.go new file mode 100644 index 0000000..e7e5016 --- /dev/null +++ b/dynamic/spec.go @@ -0,0 +1,121 @@ +package dynamic + +import ( + "fmt" + "reflect" + + "git.xdrm.io/go/aicra/api" + "git.xdrm.io/go/aicra/internal/config" +) + +// builds a spec from the configuration service +func makeSpec(service config.Service) spec { + spec := spec{ + Input: make(map[string]reflect.Type), + Output: make(map[string]reflect.Type), + } + + for _, param := range service.Input { + // make a pointer if optional + if param.Optional { + spec.Input[param.Rename] = reflect.PtrTo(param.ExtractType) + continue + } + spec.Input[param.Rename] = param.ExtractType + } + + for _, param := range service.Output { + // make a pointer if optional + if param.Optional { + spec.Output[param.Rename] = reflect.PtrTo(param.ExtractType) + continue + } + spec.Output[param.Rename] = param.ExtractType + } + + return spec +} + +// checks for HandlerFn input arguments +func (s spec) checkInput(fnt reflect.Type) error { + if fnt.NumIn() != 1 { + return ErrMissingHandlerArgument + } + + // arg[0] must be api.Request + requestArg := fnt.In(0) + if !requestArg.AssignableTo(reflect.TypeOf(api.Request{})) { + return ErrMissingRequestArgument + } + + // no input -> ok + if len(s.Input) == 0 { + return nil + } + + if fnt.NumIn() < 2 { + return ErrMissingHandlerArgumentParam + } + + // arg[1] must be a struct + structArg := fnt.In(1) + if structArg.Kind() != reflect.Struct { + return ErrMissingParamArgument + } + + // check for invlaid param + for name, ptype := range s.Input { + field, exists := structArg.FieldByName(name) + if !exists { + return fmt.Errorf("%s: %w", name, ErrMissingParamFromConfig) + } + + if !ptype.AssignableTo(field.Type) { + return fmt.Errorf("%s: %w (%s)", name, ErrWrongParamTypeFromConfig, ptype) + } + } + + return nil +} + +// checks for HandlerFn output arguments +func (s spec) checkOutput(fnt reflect.Type) error { + if fnt.NumOut() < 1 { + return ErrMissingHandlerOutput + } + + // last output must be api.Error + errOutput := fnt.Out(fnt.NumOut() - 1) + if !errOutput.AssignableTo(reflect.TypeOf(api.ErrorUnknown)) { + return ErrMissingHandlerErrorOutput + } + + // no output -> ok + if len(s.Output) == 0 { + return nil + } + + if fnt.NumOut() != 2 { + return ErrMissingParamOutput + } + + // fail if first output is not a struct + structOutput := fnt.Out(0) + if structOutput.Kind() != reflect.Struct { + return ErrMissingParamArgument + } + + // fail on invalid output + for name, ptype := range s.Output { + field, exists := structOutput.FieldByName(name) + if !exists { + return fmt.Errorf("%s: %w", name, ErrMissingOutputFromConfig) + } + + if !ptype.AssignableTo(field.Type) { + return fmt.Errorf("%s: %w (%s)", name, ErrWrongOutputTypeFromConfig, ptype) + } + } + + return nil +} diff --git a/dynamic/types.go b/dynamic/types.go new file mode 100644 index 0000000..a180e63 --- /dev/null +++ b/dynamic/types.go @@ -0,0 +1,17 @@ +package dynamic + +import "reflect" + +// HandlerFn defines a dynamic handler function +type HandlerFn interface{} + +// Handler represents a dynamic api handler +type Handler struct { + spec spec + fn HandlerFn +} + +type spec struct { + Input map[string]reflect.Type + Output map[string]reflect.Type +}