diff --git a/api/context.go b/api/context.go new file mode 100644 index 0000000..dab6f17 --- /dev/null +++ b/api/context.go @@ -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 +} diff --git a/api/middleware.go b/api/middleware.go new file mode 100644 index 0000000..3044ff7 --- /dev/null +++ b/api/middleware.go @@ -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) +} diff --git a/builder.go b/builder.go index 65c5240..e6e83ba 100644 --- a/builder.go +++ b/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" @@ -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 { diff --git a/handler.go b/handler.go index e85fe49..42dde85 100644 --- a/handler.go +++ b/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 { diff --git a/internal/dynfunc/errors.go b/internal/dynfunc/errors.go index ca87692..31eb7c5 100644 --- a/internal/dynfunc/errors.go +++ b/internal/dynfunc/errors.go @@ -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") diff --git a/internal/dynfunc/handler.go b/internal/dynfunc/handler.go index c12069c..5b941d5 100644 --- a/internal/dynfunc/handler.go +++ b/internal/dynfunc/handler.go @@ -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) diff --git a/internal/dynfunc/spec.go b/internal/dynfunc/spec.go index 571c9ae..1343f74 100644 --- a/internal/dynfunc/spec.go +++ b/internal/dynfunc/spec.go @@ -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 } diff --git a/internal/dynfunc/spec_test.go b/internal/dynfunc/spec_test.go index b8599bc..41c1ddc 100644 --- a/internal/dynfunc/spec_test.go +++ b/internal/dynfunc/spec_test.go @@ -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,