feat: add api context to handlers and add middlewares with Builder.With()
This commit is contained in:
parent
5730966d35
commit
5e5ca2d693
10
builder.go
10
builder.go
|
@ -5,6 +5,7 @@ import (
|
|||
"io"
|
||||
"net/http"
|
||||
|
||||
"git.xdrm.io/go/aicra/api"
|
||||
"git.xdrm.io/go/aicra/datatype"
|
||||
"git.xdrm.io/go/aicra/internal/config"
|
||||
"git.xdrm.io/go/aicra/internal/dynfunc"
|
||||
|
@ -14,6 +15,7 @@ import (
|
|||
type Builder struct {
|
||||
conf *config.Server
|
||||
handlers []*apiHandler
|
||||
middlewares []api.Middleware
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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
|
||||
// panics if already setup
|
||||
func (b *Builder) Setup(r io.Reader) error {
|
||||
|
|
18
handler.go
18
handler.go
|
@ -51,10 +51,20 @@ func (s Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
// 5. pass execution to the handler
|
||||
var outData, outErr = handler.dyn.Handle(input.Data)
|
||||
// 5. create context and run middlewares
|
||||
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)
|
||||
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")
|
||||
for key, values := range res.Headers {
|
||||
for _, value := range values {
|
||||
|
|
|
@ -19,6 +19,9 @@ const errMissingHandlerArgumentParam = cerr("missing handler argument : paramete
|
|||
// errUnexpectedInput - input argument is not expected
|
||||
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
|
||||
const errMissingHandlerOutput = cerr("handler must have at least 1 output")
|
||||
|
||||
|
|
|
@ -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
|
||||
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 fnv = reflect.ValueOf(h.fn)
|
||||
|
||||
|
|
|
@ -47,20 +47,31 @@ func makeSpec(service config.Service) spec {
|
|||
func (s spec) checkInput(fnv reflect.Value) error {
|
||||
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
|
||||
if len(s.Input) == 0 {
|
||||
if fnt.NumIn() > 0 {
|
||||
if fnt.NumIn() != 1 {
|
||||
return errUnexpectedInput
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if fnt.NumIn() != 1 {
|
||||
if fnt.NumIn() != 2 {
|
||||
return errMissingHandlerArgumentParam
|
||||
}
|
||||
|
||||
// arg must be a struct
|
||||
structArg := fnt.In(0)
|
||||
structArg := fnt.In(1)
|
||||
if structArg.Kind() != reflect.Struct {
|
||||
return errMissingParamArgument
|
||||
}
|
||||
|
|
|
@ -11,74 +11,109 @@ import (
|
|||
|
||||
func TestInputCheck(t *testing.T) {
|
||||
tcases := []struct {
|
||||
Name string
|
||||
Input map[string]reflect.Type
|
||||
Fn interface{}
|
||||
Err error
|
||||
}{
|
||||
// no input
|
||||
{
|
||||
Name: "no argument required, missing context",
|
||||
Input: map[string]reflect.Type{},
|
||||
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,
|
||||
},
|
||||
// func must have noarguments if none specified
|
||||
{
|
||||
Name: "argument but none required",
|
||||
Input: map[string]reflect.Type{},
|
||||
Fn: func(int, string) {},
|
||||
Fn: func(*api.Context, int) {},
|
||||
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{
|
||||
"Test1": reflect.TypeOf(int(0)),
|
||||
},
|
||||
Fn: func() {},
|
||||
Err: errMissingHandlerArgumentParam,
|
||||
Err: errMissingContext,
|
||||
},
|
||||
// input not a struct
|
||||
{
|
||||
Name: "int required, invalid context",
|
||||
Input: map[string]reflect.Type{
|
||||
"Test1": reflect.TypeOf(int(0)),
|
||||
},
|
||||
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,
|
||||
},
|
||||
// unexported param name
|
||||
{
|
||||
Name: "fail on unexported param",
|
||||
Input: map[string]reflect.Type{
|
||||
"test1": reflect.TypeOf(int(0)),
|
||||
},
|
||||
Fn: func(struct{}) {},
|
||||
Fn: func(*api.Context, struct{}) {},
|
||||
Err: errUnexportedName,
|
||||
},
|
||||
// input field missing
|
||||
{
|
||||
Name: "struct with missing field",
|
||||
Input: map[string]reflect.Type{
|
||||
"Test1": reflect.TypeOf(int(0)),
|
||||
},
|
||||
Fn: func(struct{}) {},
|
||||
Fn: func(*api.Context, struct{}) {},
|
||||
Err: errMissingParamFromConfig,
|
||||
},
|
||||
// input field invalid type
|
||||
{
|
||||
Name: "field with missing type",
|
||||
Input: map[string]reflect.Type{
|
||||
"Test1": reflect.TypeOf(int(0)),
|
||||
},
|
||||
Fn: func(struct{ Test1 string }) {},
|
||||
Fn: func(*api.Context, struct{ Test1 string }) {},
|
||||
Err: errWrongParamTypeFromConfig,
|
||||
},
|
||||
// input field valid type
|
||||
{
|
||||
Name: "valid input",
|
||||
Input: map[string]reflect.Type{
|
||||
"Test1": reflect.TypeOf(int(0)),
|
||||
},
|
||||
Fn: func(struct{ Test1 int }) {},
|
||||
Fn: func(*api.Context, struct{ Test1 int }) {},
|
||||
Err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tcase := range tcases {
|
||||
t.Run(fmt.Sprintf("case.%d", i), func(t *testing.T) {
|
||||
for _, tcase := range tcases {
|
||||
t.Run(tcase.Name, func(t *testing.T) {
|
||||
// mock spec
|
||||
s := spec{
|
||||
Input: tcase.Input,
|
||||
|
|
Loading…
Reference in New Issue