diff --git a/HISTORY.md b/HISTORY.md index f5fb1b44..b0623204 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -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. diff --git a/_benchmarks/_internal/README.md b/_benchmarks/_internal/README.md index 691fceb0..d49e2d0d 100644 --- a/_benchmarks/_internal/README.md +++ b/_benchmarks/_internal/README.md @@ -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. diff --git a/_benchmarks/_internal/v12.1.x/di_test.txt b/_benchmarks/_internal/v12.1.x/di_test.txt index 463eef58..ae8df321 100644 Binary files a/_benchmarks/_internal/v12.1.x/di_test.txt and b/_benchmarks/_internal/v12.1.x/di_test.txt differ diff --git a/_benchmarks/_internal/vNext/di.go b/_benchmarks/_internal/vNext/di.go index 6ae0e49f..451fb6be 100644 --- a/_benchmarks/_internal/vNext/di.go +++ b/_benchmarks/_internal/vNext/di.go @@ -1,8 +1,6 @@ package main -import ( - "github.com/kataras/iris/v12" -) +import "github.com/kataras/iris/v12" type ( testInput struct { diff --git a/_benchmarks/_internal/vNext/di_test.txt b/_benchmarks/_internal/vNext/di_test.txt index 11956a39..9a767c25 100644 Binary files a/_benchmarks/_internal/vNext/di_test.txt and b/_benchmarks/_internal/vNext/di_test.txt differ diff --git a/_benchmarks/_internal/vNext/go.mod b/_benchmarks/_internal/vNext/go.mod index bb58257f..190f346f 100644 --- a/_benchmarks/_internal/vNext/go.mod +++ b/_benchmarks/_internal/vNext/go.mod @@ -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 +) diff --git a/_examples/README.md b/_examples/README.md index 4fe65a3a..d7efeebf 100644 --- a/_examples/README.md +++ b/_examples/README.md @@ -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 diff --git a/_examples/dependency-injection/basic/main.go b/_examples/dependency-injection/basic/main.go new file mode 100644 index 00000000..451fb6be --- /dev/null +++ b/_examples/dependency-injection/basic/main.go @@ -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) +} diff --git a/_examples/dependency-injection/basic/middleware/main.go b/_examples/dependency-injection/basic/middleware/main.go new file mode 100644 index 00000000..2a63fb6a --- /dev/null +++ b/_examples/dependency-injection/basic/middleware/main.go @@ -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") +} diff --git a/_examples/dependency-injection/basic/middleware/main_test.go b/_examples/dependency-injection/basic/middleware/main_test.go new file mode 100644 index 00000000..eb90c87b --- /dev/null +++ b/_examples/dependency-injection/basic/middleware/main_test.go @@ -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") +} diff --git a/context/context.go b/context/context.go index 01de1ac4..39fd73c5 100644 --- a/context/context.go +++ b/context/context.go @@ -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) diff --git a/core/router/api_builder.go b/core/router/api_builder.go index f9b89e96..54143ae6 100644 --- a/core/router/api_builder.go +++ b/core/router/api_builder.go @@ -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 , 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...) } diff --git a/core/router/party.go b/core/router/party.go index 9fa74704..a8490f5e 100644 --- a/core/router/party.go +++ b/core/router/party.go @@ -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 , 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. diff --git a/hero/func_result.go b/hero/func_result.go index f9d41e4a..fee73bca 100644 --- a/hero/func_result.go +++ b/hero/func_result.go @@ -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 diff --git a/hero/handler.go b/hero/handler.go index e48036c3..3670893f 100644 --- a/hero/handler.go +++ b/hero/handler.go @@ -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 diff --git a/hero/handler_test.go b/hero/handler_test.go index 7ba2a6d1..529d2120 100644 --- a/hero/handler_test.go +++ b/hero/handler_test.go @@ -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 { diff --git a/iris.go b/iris.go index 1b3bbf01..7000fbee 100644 --- a/iris.go +++ b/iris.go @@ -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`. diff --git a/mvc/controller.go b/mvc/controller.go index 87197b34..c8563b8b 100644 --- a/mvc/controller.go +++ b/mvc/controller.go @@ -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 } diff --git a/mvc/param.go b/mvc/param.go deleted file mode 100644 index 0a3f39cf..00000000 --- a/mvc/param.go +++ /dev/null @@ -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 -}