feature: add context to handlers and middlewares #17

Closed
xdrm-brackets wants to merge 2 commits from feature/handler-context into 0.3.0
8 changed files with 122 additions and 26 deletions

13
api/context.go Normal file
View File

@ -0,0 +1,13 @@
package api
import (
"context"
"net/http"
)
// Context for api handlers
type Context struct {
context.Context
Request *http.Request
Response http.ResponseWriter
}

14
api/middleware.go Normal file
View File

@ -0,0 +1,14 @@
package api
// Middleware for the api for middle ware management (authentication, storing data)
type Middleware interface {
Handle(ctx *Context) *Context
}
// MiddlewareFunc proxies to a Middleware
type MiddlewareFunc func(ctx *Context) *Context
// Handle implements the Middleware interface
func (mwf MiddlewareFunc) Handle(ctx *Context) *Context {
return mwf(ctx)
}

View File

@ -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"
@ -12,8 +13,9 @@ import (
// Builder for an aicra server
type Builder struct {
conf *config.Server
handlers []*apiHandler
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 {

View File

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

View File

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

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

View File

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

View File

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