create dynamic package to handle reflection at runtime to check for handlers signature

This commit is contained in:
Adrien Marquès 2020-03-29 15:00:22 +02:00
parent 974f58fb8e
commit 081e48002f
Signed by: xdrm-brackets
GPG Key ID: D75243CA236D825E
4 changed files with 233 additions and 0 deletions

51
dynamic/errors.go Normal file
View File

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

44
dynamic/handler.go Normal file
View File

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

121
dynamic/spec.go Normal file
View File

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

17
dynamic/types.go Normal file
View File

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