.DI() to ConfigureContainer(...builders)

Former-commit-id: 169671a8b5b706dc8f136e68c1a060f27a2c421b
This commit is contained in:
Gerasimos (Makis) Maropoulos 2020-04-17 15:56:36 +03:00
parent eafa63da50
commit 1bb76853a9
12 changed files with 108 additions and 65 deletions

View File

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

View File

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

View File

@ -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 */

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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