From c866709accdea499ff1027b5bb2fdcb131293822 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Tue, 19 May 2020 09:28:27 +0300 Subject: [PATCH] add 'Context.Register/RemoveDependency' for registering dependencies for next handler in the chain from a common iris handler in serve-time And also, add a Configuration.FireEmptyFormError if end-dev wants to receive an iris.ErrEmptyForm error on missing form data on 'Context.ReadForm/ReadBody' Former-commit-id: a2713bec77375b2908f1f066a46be4f19e6b7a61 --- HISTORY.md | 3 + _examples/README.md | 1 + _examples/dependency-injection/jwt/main.go | 5 +- configuration.go | 22 ++++++ context/configuration.go | 5 +- context/context.go | 83 ++++++++++++++++++++++ hero/binding.go | 8 +++ hero/handler.go | 3 +- hero/handler_test.go | 54 ++++++++++++-- iris.go | 5 ++ 10 files changed, 181 insertions(+), 8 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 31d68714..e83fe684 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -371,6 +371,8 @@ Other Improvements: ![DBUG routes](https://iris-go.com/images/v12.2.0-dbug2.png?v=0) +- `Context.ReadForm` now can return an `iris.ErrEmptyForm` instead of `nil` when the new `Configuration.FireEmptyFormError` is true (or `iris.WithEmptyFormError`) on missing form body to read from. + - `Configuration.EnablePathIntelligence | iris.WithPathIntelligence` to enable path intelligence automatic path redirection on the most closest path (if any), [example]((https://github.com/kataras/iris/blob/master/_examples/routing/intelligence/main.go) - Enhanced cookie security and management through new `Context.AddCookieOptions` method and new cookie options (look on New Package-level functions section below), [securecookie](https://github.com/kataras/iris/tree/master/_examples/cookies/securecookie) example has been updated. @@ -409,6 +411,7 @@ New Package-level Variables: New Context Methods: +- `Context.RegisterDependency(v interface{})` and `Context.RemoveDependency(typ reflect.Type)` to register/remove struct dependencies on serve-time through a middleware. - `Context.SetID(id interface{})` and `Context.GetID() interface{}` added to register a custom unique indetifier to the Context, if necessary. - `Context.GetDomain() string` returns the domain. - `Context.AddCookieOptions(...CookieOption)` adds options for `SetCookie`, `SetCookieKV, UpsertCookie` and `RemoveCookie` methods for the current request. diff --git a/_examples/README.md b/_examples/README.md index 11e344d0..1b319f77 100644 --- a/_examples/README.md +++ b/_examples/README.md @@ -164,6 +164,7 @@ * [Middleware](dependency-injection/basic/middleware/main.go) * [Sessions](dependency-injection/sessions/main.go) * [Smart Contract](dependency-injection/smart-contract/main.go) + * [JWT](dependency-injection/jwt/main.go) * MVC * [Overview - Repository and Service layers](mvc/overview) * [Login - Repository and Service layers](mvc/login) diff --git a/_examples/dependency-injection/jwt/main.go b/_examples/dependency-injection/jwt/main.go index fca28654..54b0b2bf 100644 --- a/_examples/dependency-injection/jwt/main.go +++ b/_examples/dependency-injection/jwt/main.go @@ -37,10 +37,13 @@ func register(api *iris.APIContainer) { // ctx.StopWithStatus(iris.StatusUnauthorized) // return nil // } - + // // token := j.Get(ctx) // return token // }) + // ^ You can do the same with MVC too, as the container is shared and works + // the same way in both functions-as-handlers and structs-as-controllers. + // // api.Get("/", verifiedWithBindedTokenPage) } diff --git a/configuration.go b/configuration.go index 13bbfe11..e2ec56a0 100644 --- a/configuration.go +++ b/configuration.go @@ -262,6 +262,13 @@ var WithoutBodyConsumptionOnUnmarshal = func(app *Application) { app.config.DisableBodyConsumptionOnUnmarshal = true } +// WithEmptyFormError enables the setting `FireEmptyFormError`. +// +// See `Configuration`. +var WithEmptyFormError = func(app *Application) { + app.config.FireEmptyFormError = true +} + // WithoutAutoFireStatusCode disables the AutoFireStatusCode setting. // // See `Configuration`. @@ -837,6 +844,9 @@ type Configuration struct { // The body will not be changed and existing data before the // context.UnmarshalBody/ReadJSON/ReadXML will be not consumed. DisableBodyConsumptionOnUnmarshal bool `json:"disableBodyConsumptionOnUnmarshal,omitempty" yaml:"DisableBodyConsumptionOnUnmarshal" toml:"DisableBodyConsumptionOnUnmarshal"` + // FireEmptyFormError returns if set to tue true then the `context.ReadBody/ReadForm` + // will return an `iris.ErrEmptyForm` on empty request form data. + FireEmptyFormError bool `json:"fireEmptyFormError,omitempty" yaml:"FireEmptyFormError" yaml:"FireEmptyFormError"` // DisableAutoFireStatusCode if true then it turns off the http error status code handler automatic execution // from (`context.StatusCodeNotSuccessful`, defaults to < 200 || >= 400). @@ -1021,6 +1031,13 @@ func (c Configuration) GetDisableBodyConsumptionOnUnmarshal() bool { return c.DisableBodyConsumptionOnUnmarshal } +// GetFireEmptyFormError returns the Configuration.FireEmptyFormError value. +// If true then the `context.ReadBody/ReadForm` will return an `iris.ErrEmptyForm` +// on empty request form data. +func (c Configuration) GetFireEmptyFormError() bool { + return c.DisableBodyConsumptionOnUnmarshal +} + // GetDisableAutoFireStatusCode returns the Configuration#DisableAutoFireStatusCode. // Returns true when the http error status code handler automatic execution turned off. func (c Configuration) GetDisableAutoFireStatusCode() bool { @@ -1190,6 +1207,10 @@ func WithConfiguration(c Configuration) Configurator { main.DisableBodyConsumptionOnUnmarshal = v } + if v := c.FireEmptyFormError; v { + main.FireEmptyFormError = v + } + if v := c.DisableAutoFireStatusCode; v { main.DisableAutoFireStatusCode = v } @@ -1261,6 +1282,7 @@ func DefaultConfiguration() Configuration { ForceLowercaseRouting: false, FireMethodNotAllowed: false, DisableBodyConsumptionOnUnmarshal: false, + FireEmptyFormError: false, DisableAutoFireStatusCode: false, TimeFormat: "Mon, 02 Jan 2006 15:04:05 GMT", Charset: "utf-8", diff --git a/context/configuration.go b/context/configuration.go index 706dbd25..fa0cff0b 100644 --- a/context/configuration.go +++ b/context/configuration.go @@ -53,7 +53,10 @@ type ConfigurationReadOnly interface { // The body will not be changed and existing data before the // context.UnmarshalBody/ReadJSON/ReadXML will be not consumed. GetDisableBodyConsumptionOnUnmarshal() bool - + // GetFireEmptyFormError returns the Configuration.FireEmptyFormError value. + // If true then the `context.ReadBody/ReadForm` will return an `iris.ErrEmptyForm` + // on empty request form data. + GetFireEmptyFormError() bool // GetDisableAutoFireStatusCode returns the configuration.DisableAutoFireStatusCode. // Returns true when the http error status code handler automatic execution turned off. GetDisableAutoFireStatusCode() bool diff --git a/context/context.go b/context/context.go index c7af6b47..f4815096 100644 --- a/context/context.go +++ b/context/context.go @@ -662,6 +662,9 @@ type Context interface { // It supports any kind of type, including custom structs. // It will return nothing if request data are empty. // The struct field tag is "form". + // Note that it will return nil error on empty form data if `Configuration.FireEmptyFormError` + // is false (as defaulted) in this case the caller should check the pointer to + // see if something was actually binded. // // Example: https://github.com/kataras/iris/blob/master/_examples/http_request/read-form/main.go ReadForm(formObject interface{}) error @@ -1124,6 +1127,20 @@ type Context interface { // Controller returns a reflect Value of the custom Controller from which this handler executed. // It will return a Kind() == reflect.Invalid if the handler was not executed from within a controller. Controller() reflect.Value + // RegisterDependency registers a struct dependency at serve-time + // for the next handler in the chain. One value per type. + // Note that it's highly recommended to register + // your dependencies before server ran + // through APIContainer(app.ConfigureContainer) or MVC(mvc.New) + // in sake of minimum performance cost. + // + // See `UnRegisterDependency` too. + RegisterDependency(v interface{}) + // UnRegisterDependency removes a dependency based on its type. + // Reports whether a dependency with that type was found and removed successfully. + // + // See `RegisterDependency` too. + UnRegisterDependency(typ reflect.Type) bool // Application returns the iris app instance which belongs to this context. // Worth to notice that this function returns an interface @@ -2890,15 +2907,25 @@ func (ctx *context) ReadYAML(outPtr interface{}) error { // A shortcut for the `schema#IsErrPath`. var IsErrPath = schema.IsErrPath +// ErrEmptyForm is returned by `context#ReadForm` and `context#ReadBody` +// when it should read data from a request form data but there is none. +var ErrEmptyForm = errors.New("empty form") + // ReadForm binds the request body of a form to the "formObject". // It supports any kind of type, including custom structs. // It will return nothing if request data are empty. // The struct field tag is "form". +// Note that it will return nil error on empty form data if `Configuration.FireEmptyFormError` +// is false (as defaulted) in this case the caller should check the pointer to +// see if something was actually binded. // // Example: https://github.com/kataras/iris/blob/master/_examples/http_request/read-form/main.go func (ctx *context) ReadForm(formObject interface{}) error { values := ctx.FormValues() if len(values) == 0 { + if ctx.Application().ConfigurationReadOnly().GetFireEmptyFormError() { + return ErrEmptyForm + } return nil } @@ -2984,6 +3011,7 @@ func (ctx *context) ReadBody(ptr interface{}) error { // try read from query. return ctx.ReadQuery(ptr) } + // otherwise default to JSON. return ctx.ReadJSON(ptr) } @@ -5424,6 +5452,61 @@ func (ctx *context) Controller() reflect.Value { return emptyValue } +// DependenciesContextKey is the context key for the context's value +// to keep the serve-time static dependencies raw values. +const DependenciesContextKey = "iris.dependencies" + +// DependenciesMap is the type which context serve-time +// struct dependencies are stored with. +type DependenciesMap map[reflect.Type]reflect.Value + +// RegisterDependency registers a struct dependency at serve-time +// for the next handler in the chain. One value per type. +// Note that it's highly recommended to register +// your dependencies before server ran +// through APIContainer(app.ConfigureContainer) or MVC(mvc.New) +// in sake of minimum performance cost. +func (ctx *context) RegisterDependency(v interface{}) { + if v == nil { + return + } + + val, ok := v.(reflect.Value) + if !ok { + val = reflect.ValueOf(v) + } + + cv := ctx.Values().Get(DependenciesContextKey) + if cv != nil { + m, ok := cv.(DependenciesMap) + if !ok { + return + } + + m[val.Type()] = val + return + } + + ctx.Values().Set(DependenciesContextKey, DependenciesMap{ + val.Type(): val, + }) +} + +// UnRegisterDependency removes a dependency based on its type. +// Reports whether a dependency with that type was found and removed successfully. +func (ctx *context) UnRegisterDependency(typ reflect.Type) bool { + cv := ctx.Values().Get(DependenciesContextKey) + if cv != nil { + m, ok := cv.(DependenciesMap) + if ok { + delete(m, typ) + return true + } + } + + return false +} + // Application returns the iris app instance which belongs to this context. // Worth to notice that this function returns an interface // of the Application, which contains methods that are safe diff --git a/hero/binding.go b/hero/binding.go index c1c8e53f..a6ec405f 100644 --- a/hero/binding.go +++ b/hero/binding.go @@ -323,6 +323,14 @@ func payloadBinding(index int, typ reflect.Type) *binding { Handle: func(ctx context.Context, input *Input) (newValue reflect.Value, err error) { wasPtr := input.Type.Kind() == reflect.Ptr + if serveDepsV := ctx.Values().Get(context.DependenciesContextKey); serveDepsV != nil { + if serveDeps, ok := serveDepsV.(context.DependenciesMap); ok { + if newValue, ok = serveDeps[typ]; ok { + return + } + } + } + newValue = reflect.New(indirectType(input.Type)) ptr := newValue.Interface() err = ctx.ReadBody(ptr) diff --git a/hero/handler.go b/hero/handler.go index ca9512e9..225b1816 100644 --- a/hero/handler.go +++ b/hero/handler.go @@ -75,7 +75,8 @@ func makeHandler(fn interface{}, c *Container, paramsCount int) context.Handler } v := valueOf(fn) - numIn := v.Type().NumIn() + typ := v.Type() + numIn := typ.NumIn() bindings := getBindingsForFunc(v, c.Dependencies, paramsCount) diff --git a/hero/handler_test.go b/hero/handler_test.go index fdc1e905..b1ad8c73 100644 --- a/hero/handler_test.go +++ b/hero/handler_test.go @@ -129,17 +129,18 @@ func TestBindFunctionAsFunctionInputArgument(t *testing.T) { func TestPayloadBinding(t *testing.T) { h := New() - postHandler := h.Handler(func(input *testUserStruct /* ptr */) string { + ptrHandler := h.Handler(func(input *testUserStruct /* ptr */) string { return input.Username }) - postHandler2 := h.Handler(func(input testUserStruct) string { + valHandler := h.Handler(func(input testUserStruct) string { return input.Username }) app := iris.New() - app.Post("/", postHandler) - app.Post("/2", postHandler2) + app.Get("/", ptrHandler) + app.Post("/", ptrHandler) + app.Post("/2", valHandler) e := httptest.New(t, app) @@ -152,8 +153,10 @@ func TestPayloadBinding(t *testing.T) { // FORM (multipart) e.POST("/").WithMultipart().WithFormField("username", "makis").Expect().Status(httptest.StatusOK).Body().Equal("makis") - // URL query. + // POST URL query. e.POST("/").WithQuery("username", "makis").Expect().Status(httptest.StatusOK).Body().Equal("makis") + // GET URL query. + e.GET("/").WithQuery("username", "makis").Expect().Status(httptest.StatusOK).Body().Equal("makis") } /* Author's notes: @@ -241,3 +244,44 @@ func TestHandlerPathParams(t *testing.T) { testReq.Expect().Status(httptest.StatusOK).Body().Equal("42") } } + +func TestRegisterDependenciesFromContext(t *testing.T) { + // Tests serve-time struct dependencies through a common Iris middleware. + app := iris.New() + app.Use(func(ctx iris.Context) { + ctx.RegisterDependency(testUserStruct{Username: "kataras"}) + ctx.Next() + }) + app.Use(func(ctx iris.Context) { + ctx.RegisterDependency(&testServiceImpl{prefix: "say"}) + ctx.Next() + }) + + app.ConfigureContainer(func(api *iris.APIContainer) { + api.Get("/", func(u testUserStruct) string { + return u.Username + }) + + api.Get("/service", func(s *testServiceImpl) string { + return s.Say("hello") + }) + + // Note: we are not allowed to pass the service as an interface here + // because the container will, correctly, panic because it will expect + // a dependency to be registered before server ran. + api.Get("/both", func(s *testServiceImpl, u testUserStruct) string { + return s.Say(u.Username) + }) + + api.Get("/non", func() string { + return "nothing" + }) + }) + + e := httptest.New(t, app) + + e.GET("/").Expect().Status(httptest.StatusOK).Body().Equal("kataras") + e.GET("/service").Expect().Status(httptest.StatusOK).Body().Equal("say hello") + e.GET("/both").Expect().Status(httptest.StatusOK).Body().Equal("say kataras") + e.GET("/non").Expect().Status(httptest.StatusOK).Body().Equal("nothing") +} diff --git a/iris.go b/iris.go index 2c51b5f2..619bde6e 100644 --- a/iris.go +++ b/iris.go @@ -575,6 +575,11 @@ var ( // // A shortcut for the `context#IsErrPath`. IsErrPath = context.IsErrPath + // ErrEmptyForm is the type error which API users can make use of + // to check if a form was empty on `Context.ReadForm`. + // + // A shortcut for the `context#ErrEmptyForm`. + ErrEmptyForm = context.ErrEmptyForm // NewProblem returns a new Problem. // Head over to the `Problem` type godoc for more. //