add a dependency-injection examples folder for the next release and some improvements

Former-commit-id: 040168afb7caf808618f7da5e68ae8eb01cb7170
This commit is contained in:
Gerasimos (Makis) Maropoulos 2020-03-01 02:17:19 +02:00
parent 5fc24812bc
commit ce2eae9121
19 changed files with 214 additions and 76 deletions

View File

@ -72,6 +72,11 @@ func main() {
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 don't worry you can still control everything if you ever need, even errors from dependencies. Any error may occur from request-scoped dependencies or your own handler is dispatched through `Party.GetContainer().GetErrorHandler` which defaults to the `hero.DefaultErrorHandler` which sends a `400 Bad Request` response with the error's text as its body contents. If you want to handle `testInput` otherwise then just add a `Party.RegisterDependency(func(ctx iris.Context) testInput {...})` and you are ready to go.
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.HandleFunc`) do not have to require `iris.Context` just to call `ctx.Next()` anymore, this is done automatically now.
New Context Methods:
- `context.Defer(Handler)` works like `Party.Done` but for the request life-cycle.

View File

@ -14,10 +14,17 @@ Internal selected benchmarks between modified features across different versions
Measures handler factory time.
```sh
$ cd v12.1.x
$ go test -run=NONE --bench=. -count=5 --benchmem > di_test.txt
$ cd ../vNext
$ go test -run=NONE --bench=. -count=5 --benchmem > di_test.txt
```
| Name | Ops | Ns/op | B/op | Allocs/op |
|---------|:------|:--------|:--------|----|
| vNext | 181726 | 6631 | 1544 | 17 |
| v12.1.x | 96001 | 12604 | 976 | 26 |
| vNext | 184512 | 6607 | 1544 | 17 |
| v12.1.x | 95974 | 12653 | 976 | 26 |
It accepts a dynamic path parameter and a JSON request. It returns a JSON response. Fires 500000 requests with 125 concurrent connections.

View File

@ -1,8 +1,6 @@
package main
import (
"github.com/kataras/iris/v12"
)
import "github.com/kataras/iris/v12"
type (
testInput struct {

View File

@ -4,4 +4,7 @@ go 1.14
replace github.com/kataras/iris/v12 => C:/mygopath/src/github.com/kataras/iris
require github.com/kataras/iris/v12 v12.1.8
require (
github.com/kataras/iris/v12 v12.1.8
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
)

View File

@ -8,6 +8,8 @@ It doesn't always contain the "best ways" but it does cover each important featu
## Running the examples
[![Run in Postman](https://run.pstmn.io/button.svg)](https://app.getpostman.com/run-collection/bd489282b676e30de158)
1. Install the Go Programming Language, version 1.12+ from https://golang.org/dl.
2. [Install Iris](https://github.com/kataras/iris/wiki/installation)
3. Install any external packages that required by the examples

View File

@ -0,0 +1,27 @@
package main
import "github.com/kataras/iris/v12"
type (
testInput struct {
Email string `json:"email"`
}
testOutput struct {
ID int `json:"id"`
Name string `json:"name"`
}
)
func handler(id int, in testInput) testOutput {
return testOutput{
ID: id,
Name: in.Email,
}
}
func main() {
app := iris.New()
app.HandleFunc(iris.MethodPost, "/{id:int}", handler)
app.Listen(":5000", iris.WithOptimizations)
}

View File

@ -0,0 +1,61 @@
package main
import (
"errors"
"github.com/kataras/iris/v12"
)
type (
testInput struct {
Email string `json:"email"`
}
testOutput struct {
ID int `json:"id"`
Name string `json:"name"`
}
)
func handler(id int, in testInput) testOutput {
return testOutput{
ID: id,
Name: in.Email,
}
}
var errCustom = errors.New("my_error")
func middleware(in testInput) (int, error) {
if in.Email == "invalid" {
// stop the execution and don't continue to "handler"
// without firing an error.
return iris.StatusAccepted, iris.ErrStopExecution
} else if in.Email == "error" {
// stop the execution and fire a custom error.
return iris.StatusConflict, errCustom
}
return iris.StatusOK, nil
}
func newApp() *iris.Application {
app := iris.New()
// handle the route, respond with
// a JSON and 200 status code
// or 202 status code and empty body
// or a 409 status code and "my_error" body.
app.HandleFunc(iris.MethodPost, "/{id:int}", middleware, handler)
app.Configure(
iris.WithOptimizations, /* optional */
iris.WithoutBodyConsumptionOnUnmarshal /* required when more than one handler is consuming request payload(testInput) */)
return app
}
func main() {
app := newApp()
app.Listen(":8080")
}

View File

@ -0,0 +1,25 @@
package main
import (
"testing"
"github.com/kataras/iris/v12/httptest"
)
func TestDependencyInjectionBasic_Middleware(t *testing.T) {
app := newApp()
e := httptest.New(t, app)
e.POST("/42").WithJSON(testInput{Email: "my_email"}).Expect().
Status(httptest.StatusOK).
JSON().Equal(testOutput{ID: 42, Name: "my_email"})
// it should stop the execution at the middleware and return the middleware's status code,
// because the error is `ErrStopExecution`.
e.POST("/42").WithJSON(testInput{Email: "invalid"}).Expect().
Status(httptest.StatusAccepted).Body().Empty()
// it should stop the execution at the middleware and return the error's text.
e.POST("/42").WithJSON(testInput{Email: "error"}).Expect().
Status(httptest.StatusConflict).Body().Equal("my_error")
}

View File

@ -3200,13 +3200,16 @@ var (
// WriteJSON marshals the given interface object and writes the JSON response to the 'writer'.
// Ignores StatusCode, Gzip, StreamingJSON options.
func WriteJSON(writer io.Writer, v interface{}, options JSON, enableOptimization ...bool) (int, error) {
func WriteJSON(writer io.Writer, v interface{}, options JSON, optimize bool) (int, error) {
var (
result []byte
err error
optimize = len(enableOptimization) > 0 && enableOptimization[0]
result []byte
err error
)
if !optimize && options.Indent == "" {
options.Indent = " "
}
if indent := options.Indent; indent != "" {
marshalIndent := json.MarshalIndent
if optimize {
@ -3291,7 +3294,7 @@ func (ctx *context) JSON(v interface{}, opts ...JSON) (n int, err error) {
var finishCallbackB = []byte(");")
// WriteJSONP marshals the given interface object and writes the JSON response to the writer.
func WriteJSONP(writer io.Writer, v interface{}, options JSONP, enableOptimization ...bool) (int, error) {
func WriteJSONP(writer io.Writer, v interface{}, options JSONP, optimize bool) (int, error) {
if callback := options.Callback; callback != "" {
n, err := writer.Write([]byte(callback + "("))
if err != nil {
@ -3300,7 +3303,9 @@ func WriteJSONP(writer io.Writer, v interface{}, options JSONP, enableOptimizati
defer writer.Write(finishCallbackB)
}
optimize := len(enableOptimization) > 0 && enableOptimization[0]
if !optimize && options.Indent == "" {
options.Indent = " "
}
if indent := options.Indent; indent != "" {
marshalIndent := json.MarshalIndent
@ -3396,7 +3401,7 @@ func (m xmlMap) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
}
// WriteXML marshals the given interface object and writes the XML response to the writer.
func WriteXML(writer io.Writer, v interface{}, options XML) (int, error) {
func WriteXML(writer io.Writer, v interface{}, options XML, optimize bool) (int, error) {
if prefix := options.Prefix; prefix != "" {
n, err := writer.Write([]byte(prefix))
if err != nil {
@ -3404,6 +3409,10 @@ func WriteXML(writer io.Writer, v interface{}, options XML) (int, error) {
}
}
if !optimize && options.Indent == "" {
options.Indent = " "
}
if indent := options.Indent; indent != "" {
result, err := xml.MarshalIndent(v, "", indent)
if err != nil {
@ -3435,7 +3444,7 @@ func (ctx *context) XML(v interface{}, opts ...XML) (int, error) {
ctx.ContentType(ContentXMLHeaderValue)
n, err := WriteXML(ctx.writer, v, options)
n, err := WriteXML(ctx.writer, v, options, ctx.shouldOptimize())
if err != nil {
ctx.Application().Logger().Debugf("XML: %v", err)
ctx.StatusCode(http.StatusInternalServerError)

View File

@ -282,12 +282,22 @@ func (api *APIBuilder) RegisterDependency(dependency interface{}) *hero.Dependen
// can accept any input arguments that match with the Party's registered Container's `Dependencies` and
// any output result; like custom structs <T>, string, []byte, int, error,
// a combination of the above, hero.Result(hero.View | hero.Response) and more.
//
// It's common from a hero handler to not even need to accept a `Context`, for that reason,
// the "handlersFn" will call `ctx.Next()` automatically when not called manually.
// To stop the execution and not continue to the next "handlersFn"
// the end-developer should output an error and return `iris.ErrStopExecution`.
func (api *APIBuilder) HandleFunc(method, relativePath string, handlersFn ...interface{}) *Route {
handlers := make(context.Handlers, 0, len(handlersFn))
for _, h := range handlersFn {
handlers = append(handlers, api.container.Handler(h))
}
// On that type of handlers the end-developer does not have to include the Context in the handler,
// so the ctx.Next is automatically called unless an `ErrStopExecution` returned (implementation inside hero pkg).
o := ExecutionOptions{Force: true}
o.apply(&handlers)
return api.Handle(method, relativePath, handlers...)
}

View File

@ -130,6 +130,11 @@ type Party interface {
// can accept any input arguments that match with the Party's registered Container's `Dependencies` and
// any output result; like custom structs <T>, string, []byte, int, error,
// a combination of the above, hero.Result(hero.View | hero.Response) and more.
//
// It's common from a hero handler to not even need to accept a `Context`, for that reason,
// the "handlersFn" will call `ctx.Next()` automatically when not called manually.
// To stop the execution and not continue to the next "handlersFn"
// the end-developer should output an error and return `iris.ErrStopExecution`.
HandleFunc(method, relativePath string, handlersFn ...interface{}) *Route
// Handle registers a route to the server's router.

View File

@ -225,7 +225,7 @@ func dispatchFuncResult(ctx context.Context, values []reflect.Value) error {
continue
}
if statusCode < 400 {
if statusCode < 400 && value != ErrStopExecution {
statusCode = DefaultErrStatusCode
}
@ -286,7 +286,11 @@ func dispatchCommon(ctx context.Context,
if contentType == "" {
// to respect any ctx.ContentType(...) call
// especially if v is not nil.
contentType = ctx.GetContentType()
if contentType = ctx.GetContentType(); contentType == "" {
// if it's still empty set to JSON. (useful for dynamic middlewares that returns an int status code and the next handler dispatches the JSON,
// see dependency-injection/basic/middleware example)
contentType = context.ContentJSONHeaderValue
}
}
if v != nil {
@ -302,10 +306,13 @@ func dispatchCommon(ctx context.Context,
if strings.HasPrefix(contentType, context.ContentJavascriptHeaderValue) {
_, err = ctx.JSONP(v)
} else if strings.HasPrefix(contentType, context.ContentXMLHeaderValue) {
_, err = ctx.XML(v, context.XML{Indent: " "})
_, 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, context.JSON{Indent: " "})
_, err = ctx.JSON(v)
}
return err

View File

@ -18,23 +18,6 @@ func (fn ErrorHandlerFunc) HandleError(ctx context.Context, err error) {
fn(ctx, err)
}
var (
// DefaultErrStatusCode is the default error status code (400)
// when the response contains a non-nil error or a request-scoped binding error occur.
DefaultErrStatusCode = 400
// DefaultErrorHandler is the default error handler which is fired
// when a function returns a non-nil error or a request-scoped dependency failed to binded.
DefaultErrorHandler = ErrorHandlerFunc(func(ctx context.Context, err error) {
if status := ctx.GetStatusCode(); status == 0 || !context.StatusCodeNotSuccessful(status) {
ctx.StatusCode(DefaultErrStatusCode)
}
ctx.WriteString(err.Error())
ctx.StopExecution()
})
)
var (
// ErrSeeOther may be returned from a dependency handler to skip a specific dependency
// based on custom logic.
@ -45,6 +28,26 @@ var (
ErrStopExecution = fmt.Errorf("stop execution")
)
var (
// DefaultErrStatusCode is the default error status code (400)
// when the response contains a non-nil error or a request-scoped binding error occur.
DefaultErrStatusCode = 400
// DefaultErrorHandler is the default error handler which is fired
// when a function returns a non-nil error or a request-scoped dependency failed to binded.
DefaultErrorHandler = ErrorHandlerFunc(func(ctx context.Context, err error) {
if err != ErrStopExecution {
if status := ctx.GetStatusCode(); status == 0 || !context.StatusCodeNotSuccessful(status) {
ctx.StatusCode(DefaultErrStatusCode)
}
ctx.WriteString(err.Error())
}
ctx.StopExecution()
})
)
func makeHandler(fn interface{}, c *Container) context.Handler {
if fn == nil {
panic("makeHandler: function is nil")
@ -77,10 +80,12 @@ func makeHandler(fn interface{}, c *Container) context.Handler {
if err != nil {
if err == ErrSeeOther {
continue
} else if err == ErrStopExecution {
ctx.StopExecution()
return // return without error.
}
// handled inside ErrorHandler.
// else if err == ErrStopExecution {
// ctx.StopExecution()
// return // return without error.
// }
c.GetErrorHandler(ctx).HandleError(ctx, err)
return

View File

@ -156,7 +156,7 @@ before begin the implementation of it.
b.Register("user_dep", func(db myDB) User{...}).DependsOn("db")
b.Handler(func(user User) error{...})
b.Handler(func(ctx iris.Context, reuseDB myDB) {...})
Why linked over automatically? Because more thna one dependency can implement the same input and
Why linked over automatically? Because more than one dependency can implement the same input and
end-user does not care about ordering the registered ones.
Link with `DependsOn` SHOULD be optional, if exists then limit the available dependencies,
`DependsOn` SHOULD accept comma-separated values, e.g. "db, otherdep" and SHOULD also work
@ -170,6 +170,8 @@ so, in theory, end-developers could achieve same results by hand-code(inside the
26 Feb 2020. Gerasimos Maropoulos
______________________________________________
29 Feb 2020. It's done.
*/
type testMessage struct {

View File

@ -18,6 +18,8 @@ import (
// context for the handlers
"github.com/kataras/iris/v12/context"
"github.com/kataras/iris/v12/hero"
// core packages, required to build the application
"github.com/kataras/iris/v12/core/errgroup"
"github.com/kataras/iris/v12/core/host"
@ -527,6 +529,9 @@ var (
//
// A shortcut for the `context#XMLMap`.
XMLMap = context.XMLMap
// ErrStopExecution if returned from a hero middleware or a request-scope dependency
// stops the handler's execution, see _examples/dependency-injection/basic/middleware.
ErrStopExecution = hero.ErrStopExecution
)
// Constants for input argument at `router.RouteRegisterRule`.

View File

@ -341,9 +341,10 @@ func (c *ControllerActivator) handlerOf(methodName string) context.Handler {
return func(ctx context.Context) {
ctrl, err := c.injector.Acquire(ctx)
if err != nil {
if err != hero.ErrStopExecution {
c.injector.Container.GetErrorHandler(ctx).HandleError(ctx, err)
}
// if err != hero.ErrStopExecution {
// c.injector.Container.GetErrorHandler(ctx).HandleError(ctx, err)
// }
c.injector.Container.GetErrorHandler(ctx).HandleError(ctx, err)
return
}

View File

@ -1,34 +0,0 @@
package mvc
import (
"reflect"
"github.com/kataras/iris/v12/context"
"github.com/kataras/iris/v12/macro"
)
func getPathParamsForInput(startParamIndex int, params []macro.TemplateParam, funcIn ...reflect.Type) (values []reflect.Value) {
if len(funcIn) == 0 || len(params) == 0 {
return
}
consumed := make(map[int]struct{})
for _, in := range funcIn {
for j, param := range params {
if _, ok := consumed[j]; ok {
continue
}
funcDep, ok := context.ParamResolverByTypeAndIndex(in, startParamIndex+param.Index)
if !ok {
continue
}
values = append(values, funcDep)
consumed[j] = struct{}{}
break
}
}
return
}