feature: add context to handlers and middlewares #17
14
builder.go
14
builder.go
|
@ -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"
|
||||||
|
@ -12,8 +13,9 @@ import (
|
||||||
|
|
||||||
// Builder for an aicra server
|
// Builder for an aicra server
|
||||||
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 {
|
||||||
|
|
18
handler.go
18
handler.go
|
@ -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 {
|
||||||
|
|
|
@ -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")
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in New Issue