feat: add api context to handlers and add middlewares with Builder.With()
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details

This commit is contained in:
Adrien Marquès 2021-04-17 14:03:59 +02:00
parent 5730966d35
commit 5e5ca2d693
6 changed files with 95 additions and 26 deletions

View File

@ -5,6 +5,7 @@ import (
"io" "io"
"net/http" "net/http"
"git.xdrm.io/go/aicra/api"
"git.xdrm.io/go/aicra/datatype" "git.xdrm.io/go/aicra/datatype"
"git.xdrm.io/go/aicra/internal/config" "git.xdrm.io/go/aicra/internal/config"
"git.xdrm.io/go/aicra/internal/dynfunc" "git.xdrm.io/go/aicra/internal/dynfunc"
@ -14,6 +15,7 @@ import (
type Builder struct { type Builder struct {
conf *config.Server conf *config.Server
handlers []*apiHandler handlers []*apiHandler
middlewares []api.Middleware
} }
// represents an api handler (method-pattern combination) // represents an api handler (method-pattern combination)
@ -34,6 +36,14 @@ func (b *Builder) AddType(t datatype.T) {
b.conf.Types = append(b.conf.Types, t) b.conf.Types = append(b.conf.Types, t)
} }
// With adds a middleware that will be used for each request
func (b *Builder) With(mw api.Middleware) {
if b.conf == nil {
b.conf = &config.Server{}
}
b.middlewares = append(b.middlewares, mw)
}
// Setup the builder with its api definition file // Setup the builder with its api definition file
// panics if already setup // panics if already setup
func (b *Builder) Setup(r io.Reader) error { func (b *Builder) Setup(r io.Reader) error {

View File

@ -51,10 +51,20 @@ func (s Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return return
} }
// 5. pass execution to the handler // 5. create context and run middlewares
var outData, outErr = handler.dyn.Handle(input.Data) var ctx = &api.Context{
Context: r.Context(),
Request: r,
Response: w,
}
for _, mw := range s.middlewares {
ctx = mw.Handle(ctx)
}
// 6. build res from returned data // 6. pass execution to the handler
var outData, outErr = handler.dyn.Handle(ctx, input.Data)
// 7. build res from returned data
var res = api.EmptyResponse().WithError(outErr) var res = api.EmptyResponse().WithError(outErr)
for key, value := range outData { for key, value := range outData {
@ -66,7 +76,7 @@ func (s Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} }
} }
// 7. apply headers // 8. apply headers
w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Content-Type", "application/json; charset=utf-8")
for key, values := range res.Headers { for key, values := range res.Headers {
for _, value := range values { for _, value := range values {

View File

@ -19,6 +19,9 @@ const errMissingHandlerArgumentParam = cerr("missing handler argument : paramete
// errUnexpectedInput - input argument is not expected // errUnexpectedInput - input argument is not expected
const errUnexpectedInput = cerr("unexpected input struct") const errUnexpectedInput = cerr("unexpected input struct")
// errMissingContext - first input argument is missing
const errMissingContext = cerr("missing first input argument (*api.Context)")
// errMissingHandlerOutput - missing output for handler // errMissingHandlerOutput - missing output for handler
const errMissingHandlerOutput = cerr("handler must have at least 1 output") const errMissingHandlerOutput = cerr("handler must have at least 1 output")

View File

@ -46,7 +46,7 @@ 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.Context, 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)

View File

@ -47,20 +47,31 @@ func makeSpec(service config.Service) spec {
func (s spec) checkInput(fnv reflect.Value) error { func (s spec) checkInput(fnv reflect.Value) error {
fnt := fnv.Type() fnt := fnv.Type()
// fail on missing context (first arg)
if fnt.NumIn() < 1 {
return errMissingContext
}
// fail on invalid context argument
ctxArg := fnt.In(0)
if ctxArg != reflect.TypeOf(&api.Context{}) {
return errMissingContext
}
// no input -> ok // no input -> ok
if len(s.Input) == 0 { if len(s.Input) == 0 {
if fnt.NumIn() > 0 { if fnt.NumIn() != 1 {
return errUnexpectedInput return errUnexpectedInput
} }
return nil return nil
} }
if fnt.NumIn() != 1 { if fnt.NumIn() != 2 {
return errMissingHandlerArgumentParam return errMissingHandlerArgumentParam
} }
// arg must be a struct // arg must be a struct
structArg := fnt.In(0) structArg := fnt.In(1)
if structArg.Kind() != reflect.Struct { if structArg.Kind() != reflect.Struct {
return errMissingParamArgument return errMissingParamArgument
} }

View File

@ -11,74 +11,109 @@ import (
func TestInputCheck(t *testing.T) { func TestInputCheck(t *testing.T) {
tcases := []struct { tcases := []struct {
Name string
Input map[string]reflect.Type Input map[string]reflect.Type
Fn interface{} Fn interface{}
Err error Err error
}{ }{
// no input
{ {
Name: "no argument required, missing context",
Input: map[string]reflect.Type{}, Input: map[string]reflect.Type{},
Fn: func() {}, Fn: func() {},
Err: errMissingContext,
},
{
Name: "no argument required, invalid context",
Input: map[string]reflect.Type{},
Fn: func(int) {},
Err: errMissingContext,
},
{
Name: "no argument required, valid context",
Input: map[string]reflect.Type{},
Fn: func(*api.Context) {},
Err: nil, Err: nil,
}, },
// func must have noarguments if none specified
{ {
Name: "argument but none required",
Input: map[string]reflect.Type{}, Input: map[string]reflect.Type{},
Fn: func(int, string) {}, Fn: func(*api.Context, int) {},
Err: errUnexpectedInput, Err: errUnexpectedInput,
}, },
// missing input struct in func
{ {
Name: "arguments but none required",
Input: map[string]reflect.Type{},
Fn: func(*api.Context, int, string) {},
Err: errUnexpectedInput,
},
{
Name: "int required, no context",
Input: map[string]reflect.Type{ Input: map[string]reflect.Type{
"Test1": reflect.TypeOf(int(0)), "Test1": reflect.TypeOf(int(0)),
}, },
Fn: func() {}, Fn: func() {},
Err: errMissingHandlerArgumentParam, Err: errMissingContext,
}, },
// input not a struct
{ {
Name: "int required, invalid context",
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(int) {},
Err: errMissingContext,
},
{
Name: "int required, only context provided",
Input: map[string]reflect.Type{
"Test1": reflect.TypeOf(int(0)),
},
Fn: func(*api.Context) {},
Err: errMissingHandlerArgumentParam,
},
{
Name: "non-struct second argument",
Input: map[string]reflect.Type{
"Test1": reflect.TypeOf(int(0)),
},
Fn: func(*api.Context, int) {},
Err: errMissingParamArgument, Err: errMissingParamArgument,
}, },
// unexported param name
{ {
Name: "fail on unexported param",
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{}) {},
Err: errUnexportedName, Err: errUnexportedName,
}, },
// input field missing
{ {
Name: "struct with missing field",
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{}) {},
Err: errMissingParamFromConfig, Err: errMissingParamFromConfig,
}, },
// input field invalid type
{ {
Name: "field with missing type",
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 }) {},
Err: errWrongParamTypeFromConfig, Err: errWrongParamTypeFromConfig,
}, },
// input field valid type
{ {
Name: "valid input",
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 }) {},
Err: nil, Err: nil,
}, },
} }
for i, tcase := range tcases { for _, tcase := range tcases {
t.Run(fmt.Sprintf("case.%d", i), func(t *testing.T) { t.Run(tcase.Name, func(t *testing.T) {
// mock spec // mock spec
s := spec{ s := spec{
Input: tcase.Input, Input: tcase.Input,