From 1bb76853a92b4405423be875327caa58a78a0965 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Fri, 17 Apr 2020 15:56:36 +0300 Subject: [PATCH] .DI() to ConfigureContainer(...builders) Former-commit-id: 169671a8b5b706dc8f136e68c1a060f27a2c421b --- HISTORY.md | 14 ++++-- _examples/dependency-injection/basic/main.go | 4 +- .../basic/middleware/main.go | 9 ++-- context/context.go | 11 ++--- core/router/api_builder.go | 24 ++++++++--- .../{api_builder_di.go => api_container.go} | 43 ++++++++++--------- core/router/party.go | 12 ++++-- go19.go | 5 +++ hero/binding.go | 4 +- hero/func_result.go | 35 +++++++++------ hero/handler_test.go | 10 ++--- mvc/mvc.go | 2 +- 12 files changed, 108 insertions(+), 65 deletions(-) rename core/router/{api_builder_di.go => api_container.go} (83%) diff --git a/HISTORY.md b/HISTORY.md index 8953b825..5741925f 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -34,7 +34,7 @@ The most common scenario from a route to handle is to: - accept one or more path parameters and request data, a payload - send back a response, a payload (JSON, XML,...) -The new Iris Dependency Injection feature is about **33.2% faster** than its predecessor on the above case. This drops down even more the performance cost between native handlers and dynamic handlers with dependencies. This reason itself brings us, with safety and performance-wise, to the new `Party.DI() *APIBuilderDI` method which returns methods such as `DI.Handle(method, relativePath string, handlersFn ...interface{}) *Route` and `DI.RegisterDependency`. +The new Iris Dependency Injection feature is about **33.2% faster** than its predecessor on the above case. This drops down even more the performance cost between native handlers and dynamic handlers with dependencies. This reason itself brings us, with safety and performance-wise, to the new `Party.ConfigureContainer(builder ...func(*iris.APIContainer)) *APIContainer` method which returns methods such as `Handle(method, relativePath string, handlersFn ...interface{}) *Route` and `RegisterDependency`. Look how clean your codebase can be when using Iris': @@ -63,12 +63,14 @@ func handler(id int, in testInput) testOutput { func main() { app := iris.New() - app.DI().Handle(iris.MethodPost, "/{id:int}", handler) + app.ConfigureContainer(func(api *iris.APIContainer) { + api.Post("/{id:int}", handler) + }) app.Listen(":5000", iris.WithOptimizations) } ``` -Your eyes don't lie you. You read well, no `ctx.ReadJSON(&v)` and `ctx.JSON(send)` neither `error` handling are presented. It is a huge relief but if you ever need, you still have the control over those, even errors from dependencies. Any error may occur from request-scoped dependencies or your own handler is dispatched through `Party.DI().Container.GetErrorHandler` which defaults to the `hero.DefaultErrorHandler` which sends a `400 Bad Request` response with the error's text as its body contents, you can change it through `Party.DI().OnError`. If you want to handle `testInput` otherwise then just add a `Party.DI().RegisterDependency(func(ctx iris.Context) testInput {...})` and you are ready to go. Here is a quick list of the new Party.DI's fields and methods: +Your eyes don't lie you. You read well, no `ctx.ReadJSON(&v)` and `ctx.JSON(send)` neither `error` handling are presented. It is a huge relief but if you ever need, you still have the control over those, even errors from dependencies. Here is a quick list of the new Party.ConfigureContainer()'s fields and methods: ```go // Container holds the DI Container of this Party featured Dependency Injection. @@ -122,6 +124,10 @@ Done(handlersFn ...interface{}) // To stop the execution and not continue to the next "handlersFn" // the end-developer should output an error and return `iris.ErrStopExecution`. Handle(method, relativePath string, handlersFn ...interface{}) *Route + +// Get registers a GET route, same as `Handle("GET", relativePath, handlersFn....)`. +Get(relativePath string, handlersFn ...interface{}) *Route +// and so on... ``` Prior to this version the `iris.Context` was the only one dependency that has been automatically binded to the handler's input or a controller's fields and methods, read below to see what types are automatically binded: @@ -165,7 +171,7 @@ Other Improvements: - `ctx.JSON, JSONP, XML`: if `iris.WithOptimizations` is NOT passed on `app.Run/Listen` then the indentation defaults to `" "` (two spaces) otherwise it is empty or the provided value. -- Hero Handlers (and `app.DI().Handle`) do not have to require `iris.Context` just to call `ctx.Next()` anymore, this is done automatically now. +- Hero Handlers (and `app.ConfigureContainer().Handle`) do not have to require `iris.Context` just to call `ctx.Next()` anymore, this is done automatically now. New Context Methods: diff --git a/_examples/dependency-injection/basic/main.go b/_examples/dependency-injection/basic/main.go index a2d5ef04..3fd838d3 100644 --- a/_examples/dependency-injection/basic/main.go +++ b/_examples/dependency-injection/basic/main.go @@ -22,6 +22,8 @@ func handler(id int, in testInput) testOutput { func main() { app := iris.New() - app.DI().Post("/{id:int}", handler) + app.ConfigureContainer(func(api *iris.APIContainer) { + api.Post("/{id:int}", handler) + }) app.Listen(":8080") } diff --git a/_examples/dependency-injection/basic/middleware/main.go b/_examples/dependency-injection/basic/middleware/main.go index 9e348b38..6faf607d 100644 --- a/_examples/dependency-injection/basic/middleware/main.go +++ b/_examples/dependency-injection/basic/middleware/main.go @@ -46,11 +46,10 @@ func newApp() *iris.Application { // a JSON and 200 status code // or 202 status code and empty body // or a 409 status code and "my_error" body. - di := app.DI() - { - di.Use(middleware) - di.Post("/{id:int}", handler) - } + app.ConfigureContainer(func(api *iris.APIContainer) { + api.Use(middleware) + api.Post("/{id:int}", handler) + }) app.Configure( iris.WithOptimizations, /* optional */ diff --git a/context/context.go b/context/context.go index c1a61100..5eccd251 100644 --- a/context/context.go +++ b/context/context.go @@ -1994,18 +1994,19 @@ func (ctx *context) GetContentType() string { return ctx.writer.Header().Get(ContentTypeHeaderKey) } -func trimHeaderValue(cType string) string { - for i, char := range cType { +// TrimHeaderValue returns the "v[0:first space or semicolon]". +func TrimHeaderValue(v string) string { + for i, char := range v { if char == ' ' || char == ';' { - return cType[:i] + return v[:i] } } - return cType + return v } // GetContentType returns the request's header value of "Content-Type". func (ctx *context) GetContentTypeRequested() string { - return trimHeaderValue(ctx.GetHeader(ContentTypeHeaderKey)) + return TrimHeaderValue(ctx.GetHeader(ContentTypeHeaderKey)) } // GetContentLength returns the request's header value of "Content-Length". diff --git a/core/router/api_builder.go b/core/router/api_builder.go index 45fdb7c7..0118cd94 100644 --- a/core/router/api_builder.go +++ b/core/router/api_builder.go @@ -113,7 +113,7 @@ func (repo *repository) register(route *Route, rule RouteRegisterRule) (*Route, // and child routers. type APIBuilder struct { // the per-party APIBuilder with DI. - apiBuilderDI *APIBuilderDI + apiBuilderDI *APIContainer // the api builder global macros registry macros *macro.Macros @@ -171,7 +171,7 @@ func NewAPIBuilder() *APIBuilder { routes: new(repository), } - api.apiBuilderDI = &APIBuilderDI{ + api.apiBuilderDI = &APIContainer{ Self: api, Container: hero.New(), } @@ -179,8 +179,22 @@ func NewAPIBuilder() *APIBuilder { return api } -// DI returns the APIBuilder featured with Dependency Injection. -func (api *APIBuilder) DI() *APIBuilderDI { +// ConfigureContainer accepts one or more functions that can be used +// to configure dependency injection features of this Party +// such as register dependency and register handlers that will automatically inject any valid dependency. +// However, if the "builder" parameter is nil or not provided then it just returns the *APIContainer, +// which automatically initialized on Party allocation. +// +// It returns the same `APIBuilder` featured with Dependency Injection. +func (api *APIBuilder) ConfigureContainer(builder ...func(*APIContainer)) *APIContainer { + for _, b := range builder { + if b == nil { + continue + } + + b(api.apiBuilderDI) + } + return api.apiBuilderDI } @@ -529,7 +543,7 @@ func (api *APIBuilder) Party(relativePath string, handlers ...context.Handler) P // based on the fullpath. childContainer := api.apiBuilderDI.Container.Clone() - childAPI.apiBuilderDI = &APIBuilderDI{ + childAPI.apiBuilderDI = &APIContainer{ Self: childAPI, Container: childContainer, } diff --git a/core/router/api_builder_di.go b/core/router/api_container.go similarity index 83% rename from core/router/api_builder_di.go rename to core/router/api_container.go index 67d601a9..2fde365f 100644 --- a/core/router/api_builder_di.go +++ b/core/router/api_container.go @@ -8,8 +8,9 @@ import ( "github.com/kataras/iris/v12/macro" ) -// APIBuilderDI is a wrapper of a common `Party` features Dependency Injection. -type APIBuilderDI struct { +// APIContainer is a wrapper of a common `Party` featured by Dependency Injection. +// See `Party.ConfigureContainer` for more. +type APIContainer struct { // Self returns the original `Party` without DI features. Self Party @@ -17,12 +18,12 @@ type APIBuilderDI struct { Container *hero.Container } -// Party returns a child of this `APIBuilderDI` featured with Dependency Injection. +// Party returns a child of this `APIContainer` featured with Dependency Injection. // Like the `Self.Party` method does for the common Router Groups. -func (api *APIBuilderDI) Party(relativePath string, handlersFn ...interface{}) *APIBuilderDI { +func (api *APIContainer) Party(relativePath string, handlersFn ...interface{}) *APIContainer { handlers := api.convertHandlerFuncs(relativePath, handlersFn...) p := api.Self.Party(relativePath, handlers...) - return p.DI() + return p.ConfigureContainer() } // OnError adds an error handler for this Party's DI Hero Container and its handlers (or controllers). @@ -33,7 +34,7 @@ func (api *APIBuilderDI) Party(relativePath string, handlersFn ...interface{}) * // Container.GetErrorHandler = func(ctx iris.Context) hero.ErrorHandler { return errorHandler } // // See `RegisterDependency`, `Use`, `Done` and `Handle` too. -func (api *APIBuilderDI) OnError(errorHandler func(context.Context, error)) { +func (api *APIContainer) OnError(errorHandler func(context.Context, error)) { errHandler := hero.ErrorHandlerFunc(errorHandler) api.Container.GetErrorHandler = func(ctx context.Context) hero.ErrorHandler { return errHandler @@ -59,12 +60,12 @@ func (api *APIBuilderDI) OnError(errorHandler func(context.Context, error)) { // - RegisterDependency(func(User) OtherResponse {...}) // // See `OnError`, `Use`, `Done` and `Handle` too. -func (api *APIBuilderDI) RegisterDependency(dependency interface{}) *hero.Dependency { +func (api *APIContainer) RegisterDependency(dependency interface{}) *hero.Dependency { return api.Container.Register(dependency) } // convertHandlerFuncs accepts Iris hero handlers and returns a slice of native Iris handlers. -func (api *APIBuilderDI) convertHandlerFuncs(relativePath string, handlersFn ...interface{}) context.Handlers { +func (api *APIContainer) convertHandlerFuncs(relativePath string, handlersFn ...interface{}) context.Handlers { fullpath := api.Self.GetRelPath() + relativePath paramsCount := macro.CountParams(fullpath, *api.Self.Macros()) @@ -84,14 +85,14 @@ func (api *APIBuilderDI) convertHandlerFuncs(relativePath string, handlersFn ... // Use same as `Self.Use` but it accepts dynamic functions as its "handlersFn" input. // // See `OnError`, `RegisterDependency`, `Done` and `Handle` for more. -func (api *APIBuilderDI) Use(handlersFn ...interface{}) { +func (api *APIContainer) Use(handlersFn ...interface{}) { handlers := api.convertHandlerFuncs("/", handlersFn...) api.Self.Use(handlers...) } // Done same as `Self.Done` but it accepts dynamic functions as its "handlersFn" input. // See `OnError`, `RegisterDependency`, `Use` and `Handle` for more. -func (api *APIBuilderDI) Done(handlersFn ...interface{}) { +func (api *APIContainer) Done(handlersFn ...interface{}) { handlers := api.convertHandlerFuncs("/", handlersFn...) api.Self.Done(handlers...) } @@ -107,7 +108,7 @@ func (api *APIBuilderDI) Done(handlersFn ...interface{}) { // the end-developer should output an error and return `iris.ErrStopExecution`. // // See `OnError`, `RegisterDependency`, `Use`, `Done`, `Get`, `Post`, `Put`, `Patch` and `Delete` too. -func (api *APIBuilderDI) Handle(method, relativePath string, handlersFn ...interface{}) *Route { +func (api *APIContainer) Handle(method, relativePath string, handlersFn ...interface{}) *Route { handlers := api.convertHandlerFuncs(relativePath, handlersFn...) return api.Self.Handle(method, relativePath, handlers...) } @@ -115,63 +116,63 @@ func (api *APIBuilderDI) Handle(method, relativePath string, handlersFn ...inter // Get registers a route for the Get HTTP Method. // // Returns a *Route and an error which will be filled if route wasn't registered successfully. -func (api *APIBuilderDI) Get(relativePath string, handlersFn ...interface{}) *Route { +func (api *APIContainer) Get(relativePath string, handlersFn ...interface{}) *Route { return api.Handle(http.MethodGet, relativePath, handlersFn...) } // Post registers a route for the Post HTTP Method. // // Returns a *Route and an error which will be filled if route wasn't registered successfully. -func (api *APIBuilderDI) Post(relativePath string, handlersFn ...interface{}) *Route { +func (api *APIContainer) Post(relativePath string, handlersFn ...interface{}) *Route { return api.Handle(http.MethodPost, relativePath, handlersFn...) } // Put registers a route for the Put HTTP Method. // // Returns a *Route and an error which will be filled if route wasn't registered successfully. -func (api *APIBuilderDI) Put(relativePath string, handlersFn ...interface{}) *Route { +func (api *APIContainer) Put(relativePath string, handlersFn ...interface{}) *Route { return api.Handle(http.MethodPut, relativePath, handlersFn...) } // Delete registers a route for the Delete HTTP Method. // // Returns a *Route and an error which will be filled if route wasn't registered successfully. -func (api *APIBuilderDI) Delete(relativePath string, handlersFn ...interface{}) *Route { +func (api *APIContainer) Delete(relativePath string, handlersFn ...interface{}) *Route { return api.Handle(http.MethodDelete, relativePath, handlersFn...) } // Connect registers a route for the Connect HTTP Method. // // Returns a *Route and an error which will be filled if route wasn't registered successfully. -func (api *APIBuilderDI) Connect(relativePath string, handlersFn ...interface{}) *Route { +func (api *APIContainer) Connect(relativePath string, handlersFn ...interface{}) *Route { return api.Handle(http.MethodConnect, relativePath, handlersFn...) } // Head registers a route for the Head HTTP Method. // // Returns a *Route and an error which will be filled if route wasn't registered successfully. -func (api *APIBuilderDI) Head(relativePath string, handlersFn ...interface{}) *Route { +func (api *APIContainer) Head(relativePath string, handlersFn ...interface{}) *Route { return api.Handle(http.MethodHead, relativePath, handlersFn...) } // Options registers a route for the Options HTTP Method. // // Returns a *Route and an error which will be filled if route wasn't registered successfully. -func (api *APIBuilderDI) Options(relativePath string, handlersFn ...interface{}) *Route { +func (api *APIContainer) Options(relativePath string, handlersFn ...interface{}) *Route { return api.Handle(http.MethodOptions, relativePath, handlersFn...) } // Patch registers a route for the Patch HTTP Method. // // Returns a *Route and an error which will be filled if route wasn't registered successfully. -func (api *APIBuilderDI) Patch(relativePath string, handlersFn ...interface{}) *Route { +func (api *APIContainer) Patch(relativePath string, handlersFn ...interface{}) *Route { return api.Handle(http.MethodPatch, relativePath, handlersFn...) } // Trace registers a route for the Trace HTTP Method. // // Returns a *Route and an error which will be filled if route wasn't registered successfully. -func (api *APIBuilderDI) Trace(relativePath string, handlersFn ...interface{}) *Route { +func (api *APIContainer) Trace(relativePath string, handlersFn ...interface{}) *Route { return api.Handle(http.MethodTrace, relativePath, handlersFn...) } @@ -185,7 +186,7 @@ func (api *APIBuilderDI) Trace(relativePath string, handlersFn ...interface{}) * // Options // Connect // Trace -func (api *APIBuilderDI) Any(relativePath string, handlersFn ...interface{}) (routes []*Route) { +func (api *APIContainer) Any(relativePath string, handlersFn ...interface{}) (routes []*Route) { handlers := api.convertHandlerFuncs(relativePath, handlersFn...) for _, m := range AllMethods { diff --git a/core/router/party.go b/core/router/party.go index 543a4df7..74e82566 100644 --- a/core/router/party.go +++ b/core/router/party.go @@ -9,10 +9,16 @@ import ( // Party is just a group joiner of routes which have the same prefix and share same middleware(s) also. // Party could also be named as 'Join' or 'Node' or 'Group' , Party chosen because it is fun. // -// Look the "APIBuilder" for its implementation. +// Look the `APIBuilder` structure for its implementation. type Party interface { - // DI returns the APIBuilder featured with Dependency Injection. - DI() *APIBuilderDI + // ConfigureContainer accepts one or more functions that can be used + // to configure dependency injection features of this Party + // such as register dependency and register handlers that will automatically inject any valid dependency. + // However, if the "builder" parameter is nil or not provided then it just returns the *APIContainer, + // which automatically initialized on Party allocation. + // + // It returns the same `APIBuilder` featured with Dependency Injection. + ConfigureContainer(builder ...func(*APIContainer)) *APIContainer // GetRelPath returns the current party's relative path. // i.e: diff --git a/go19.go b/go19.go index b7582256..4930431c 100644 --- a/go19.go +++ b/go19.go @@ -83,6 +83,11 @@ type ( // // A shortcut for the `core/router#Party`, useful when `PartyFunc` is being used. Party = router.Party + // APIContainer is a wrapper of a common `Party` featured by Dependency Injection. + // See `Party.ConfigureContainer` for more. + // + // A shortcut for the `core/router#APIContainer`. + APIContainer = router.APIContainer // DirOptions contains the optional settings that // `FileServer` and `Party#HandleDir` can use to serve files and assets. // A shortcut for the `router.DirOptions`, useful when `FileServer` or `HandleDir` is being used. diff --git a/hero/binding.go b/hero/binding.go index 89300efe..c1c8e53f 100644 --- a/hero/binding.go +++ b/hero/binding.go @@ -118,8 +118,8 @@ func getBindingsFor(inputs []reflect.Type, deps []*Dependency, paramsCount int) // That way the above will work as expected: // 1. mvc.New(app.Party("/path/{firstparam}")).Handle(....Controller.GetBy(secondparam string)) // 2. mvc.New(app.Party("/path/{firstparam}/{secondparam}")).Handle(...Controller.GetBy(firstparam, secondparam string)) - // 3. usersRouter := app.Party("/users/{id:uint64}"); usersRouter.DI().Handle(method, "/", handler(id uint64)) - // 4. usersRouter.Party("/friends").DI().Handle(method, "/{friendID:uint64}", handler(friendID uint64)) + // 3. usersRouter := app.Party("/users/{id:uint64}"); usersRouter.ConfigureContainer().Handle(method, "/", handler(id uint64)) + // 4. usersRouter.Party("/friends").ConfigureContainer().Handle(method, "/{friendID:uint64}", handler(friendID uint64)) // // Therefore, count the inputs that can be path parameters first. shouldBindParams := make(map[int]struct{}) diff --git a/hero/func_result.go b/hero/func_result.go index 98f43943..58b6ff77 100644 --- a/hero/func_result.go +++ b/hero/func_result.go @@ -4,6 +4,7 @@ import ( "reflect" "strings" + "github.com/golang/protobuf/proto" "github.com/kataras/iris/v12/context" "github.com/fatih/structs" @@ -323,21 +324,29 @@ func dispatchCommon(ctx context.Context, return nil } - var err error + switch context.TrimHeaderValue(contentType) { + case context.ContentXMLHeaderValue: + _, err := ctx.XML(v) + return err + case context.ContentYAMLHeaderValue: + _, err := ctx.YAML(v) + return err + case context.ContentProtobufHeaderValue: + msg, ok := v.(proto.Message) + if !ok { + return context.ErrContentNotSupported + } - if strings.HasPrefix(contentType, context.ContentJavascriptHeaderValue) { - _, err = ctx.JSONP(v) - } else if strings.HasPrefix(contentType, context.ContentXMLHeaderValue) { - _, err = ctx.XML(v) - // no need: context.XML{Indent: " "}), after v12.2, - // if not iris.WithOptimizations passed and indent is empty then it sets it to two spaces for JSON, JSONP and XML, - // otherwise given indentation. - } else { - // defaults to json if content type is missing or its application/json. - _, err = ctx.JSON(v) + _, err := ctx.Protobuf(msg) + return err + case context.ContentMsgPackHeaderValue, context.ContentMsgPack2HeaderValue: + _, err := ctx.MsgPack(v) + return err + default: + // otherwise default to JSON. + _, err := ctx.JSON(v) + return err } - - return err } ctx.ContentType(contentType) diff --git a/hero/handler_test.go b/hero/handler_test.go index 3c938701..fdc1e905 100644 --- a/hero/handler_test.go +++ b/hero/handler_test.go @@ -220,16 +220,16 @@ func TestHandlerPathParams(t *testing.T) { return fmt.Sprintf("%d", id) } - app.PartyFunc("/users", func(r iris.Party) { - r.DI().Get("/{id:uint64}", handler) + app.Party("/users").ConfigureContainer(func(api *iris.APIContainer) { + api.Get("/{id:uint64}", handler) }) - app.PartyFunc("/editors/{id:uint64}", func(r iris.Party) { - r.DI().Get("/", handler) + app.Party("/editors/{id:uint64}").ConfigureContainer(func(api *iris.APIContainer) { + api.Get("/", handler) }) // should receive the last one, as we expected only one useful for MVC (there is a similar test there too). - app.DI().Get("/{ownerID:uint64}/book/{booKID:uint64}", handler) + app.ConfigureContainer().Get("/{ownerID:uint64}/book/{booKID:uint64}", handler) e := httptest.New(t, app) diff --git a/mvc/mvc.go b/mvc/mvc.go index fe2eb3db..549c99de 100644 --- a/mvc/mvc.go +++ b/mvc/mvc.go @@ -44,7 +44,7 @@ func newApp(subRouter router.Party, container *hero.Container) *Application { // // Example: `New(app.Party("/todo"))` or `New(app)` as it's the same as `New(app.Party("/"))`. func New(party router.Party) *Application { - return newApp(party, party.DI().Container) + return newApp(party, party.ConfigureContainer().Container) } // Configure creates a new controller and configures it,