From f54dc697cc4859d3785321558265c5ac9bf1fb33 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Fri, 16 Dec 2016 10:20:05 +0200 Subject: [PATCH] More on Transactions: iris.UseTransaction and iris.DoneTransaction. See HISTORY.md --- HISTORY.md | 6 ++- README.md | 4 +- context.go | 35 ++++++++++----- context_test.go | 114 ++++++++++++++++++++++++++++++++++++++++++++---- iris.go | 82 +++++++++++++++++++++++++++++----- 5 files changed, 208 insertions(+), 33 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 0605aa6c..b9582fcc 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -2,10 +2,14 @@ **How to upgrade**: remove your `$GOPATH/src/github.com/kataras` folder, open your command-line and execute this command: `go get -u github.com/kataras/iris/iris`. +## 5.1.1 -> 5.1.2 + +- **More on Transactions vol 2**: Added **iris.UseTransaction** and **iris.DoneTransaction** to register transactions as you register middleware(handlers). new named type **iris.TransactionFunc**, shortcut of `func(scope *iris.TransactionScope)`, that gives you a function which you can convert a transaction to a normal handler/middleware using its `.ToMiddleware()`, for more see the `test code inside context_test.go:TestTransactionsMiddleware`. + ## 5.1.0 -> 5.1.1 Two hours after the previous update, -- **More on Transactions**: By-default transaction's lifetime is 'per-call/transient' meaning that each transaction has its own scope on the context, rollbacks when `scope.Complete(notNilAndNotEmptyError)` and the rest of transictions in chain are executed as expected, from now and on you have the ability to `skip the rest of the next transictions on first failure` by simply call `scope.RequestScoped(true)`. +- **More on Transactions**: By-default transaction's lifetime is 'per-call/transient' meaning that each transaction has its own scope on the context, rollbacks when `scope.Complete(notNilAndNotEmptyError)` and the rest of transactions in chain are executed as expected, from now and on you have the ability to `skip the rest of the next transactions on first failure` by simply call `scope.RequestScoped(true)`. Note: `RequestTransactionScope` renamed to ,simply, `TransactionScope`. diff --git a/README.md b/README.md index 0dcf44d1..f1cd76d2 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@
-Releases +Releases Examples @@ -920,7 +920,7 @@ I recommend writing your API tests using this new library, [httpexpect](https:// Versioning ------------ -Current: **v5.1.1** +Current: **v5.1.2** Stable: **[v4 LTS](https://github.com/kataras/iris/tree/4.0.0#versioning)** diff --git a/context.go b/context.go index 9fc6737d..b579c001 100644 --- a/context.go +++ b/context.go @@ -1161,13 +1161,6 @@ func NewErrWithStatus() *ErrWithStatus { return new(ErrWithStatus) } -// TransactionValidator used to register global transaction pre-validators -type TransactionValidator interface { - // ValidateTransaction pre-validates transactions - // transaction fails if this returns an error or it's Complete has a non empty error - ValidateTransaction(*TransactionScope) error -} - // TransactionScope is the (request) transaction scope of a handler's context // Can't say a lot here because I it will take more than 200 lines to write about. // You can search third-party articles or books on how Business Transaction works (it's quite simple, especialy here). @@ -1222,7 +1215,9 @@ func (r *TransactionScope) Complete(err error) { return } } - } else if reason == "" { + } + + if reason == "" { // do nothing empty reason and no status code means that this is not a failure, even if the error is not nil. return } @@ -1275,10 +1270,28 @@ func (ctx *Context) TransactionsSkipped() bool { return false } -// BeginTransaction starts a request scoped transaction. +// TransactionFunc is just a func(scope *TransactionScope) +// used to register transaction(s) to a handler's context. +type TransactionFunc func(scope *TransactionScope) + +// ToMiddleware wraps/converts a transaction to a handler func +// it is not recommended to be used by users because +// this can be a little tricky if someone doesn't know how transaction works. // -// See more here: https://github.com/iris-contrib/examples/tree/master/transactions -func (ctx *Context) BeginTransaction(pipe func(scope *TransactionScope)) { +// Note: it auto-calls the ctx.Next() so as I noted, not recommended to use if you don't know the code behind it, +// use the .UseTransaction and .DoneTransaction instead +func (pipe TransactionFunc) ToMiddleware() HandlerFunc { + return func(ctx *Context) { + ctx.BeginTransaction(pipe) + ctx.Next() + } +} + +// BeginTransaction starts a request scoped transaction. +// Transactions have their own middleware ecosystem also, look iris.go:UseTransaction. +// +// See https://github.com/iris-contrib/examples/tree/master/transactions for more +func (ctx *Context) BeginTransaction(pipe TransactionFunc) { // do NOT begin a transaction when the previous transaction has been failed // and it was requested scoped or SkipTransactions called manually if ctx.TransactionsSkipped() { diff --git a/context_test.go b/context_test.go index 7b12a7a0..59b33de5 100644 --- a/context_test.go +++ b/context_test.go @@ -783,13 +783,13 @@ func TestTemplatesDisabled(t *testing.T) { func TestTransactions(t *testing.T) { iris.ResetDefault() - firstTransictionFailureMessage := "Error: Virtual failure!!!" - secondTransictionSuccessHTMLMessage := "

This will sent at all cases because it lives on different transaction and it doesn't fails

" + firstTransactionFailureMessage := "Error: Virtual failure!!!" + secondTransactionSuccessHTMLMessage := "

This will sent at all cases because it lives on different transaction and it doesn't fails

" persistMessage := "

I persist show this message to the client!

" maybeFailureTransaction := func(shouldFail bool, isRequestScoped bool) func(scope *iris.TransactionScope) { return func(scope *iris.TransactionScope) { - // OPTIONAl, if true then the next transictions will not be executed if this transiction fails + // OPTIONAl, if true then the next transactions will not be executed if this transaction fails scope.RequestScoped(isRequestScoped) // OPTIONAL STEP: @@ -815,7 +815,7 @@ func TestTransactions(t *testing.T) { if fail { err.Status(iris.StatusInternalServerError). // if status given but no reason then the default or the custom http error will be fired (like ctx.EmitError) - Reason(firstTransictionFailureMessage) + Reason(firstTransactionFailureMessage) } // OPTIONAl STEP: @@ -828,7 +828,7 @@ func TestTransactions(t *testing.T) { successTransaction := func(scope *iris.TransactionScope) { scope.Context.HTML(iris.StatusOK, - secondTransictionSuccessHTMLMessage) + secondTransactionSuccessHTMLMessage) // * if we don't have any 'throw error' logic then no need of scope.Complete() } @@ -861,18 +861,116 @@ func TestTransactions(t *testing.T) { Status(iris.StatusOK). ContentType("text/html", iris.Config.Charset). Body(). - Equal(firstTransictionFailureMessage + secondTransictionSuccessHTMLMessage + persistMessage) + Equal(firstTransactionFailureMessage + secondTransactionSuccessHTMLMessage + persistMessage) e.GET("/failFirsTransactionButSuccessSecond"). Expect(). Status(iris.StatusOK). ContentType("text/html", iris.Config.Charset). Body(). - Equal(firstTransictionFailureMessage + secondTransictionSuccessHTMLMessage) + Equal(firstTransactionFailureMessage + secondTransactionSuccessHTMLMessage) e.GET("/failAllBecauseOfRequestScopeAndFailure"). Expect(). Status(iris.StatusInternalServerError). Body(). - Equal(firstTransictionFailureMessage) + Equal(firstTransactionFailureMessage) +} + +func TestTransactionsMiddleware(t *testing.T) { + forbiddenMsg := "Error: Not allowed." + allowMsg := "Hello!" + + transaction := iris.TransactionFunc(func(scope *iris.TransactionScope) { + // must set that to true when we want to bypass the whole handler if this transaction fails. + scope.RequestScoped(true) + // optional but useful when we want a specific reason message + // without register global custom http errors to a status (using iris.OnError) + err := iris.NewErrWithStatus() + // the difference from ctx.BeginTransaction is that + // if that fails it not only skips all transactions but all next handler(s) too + // here we use this middleware AFTER a handler, so all handlers are executed before that but + // this will fail because this is the difference from normal handler, it resets the whole response if Complete(notEmptyError) + if scope.Context.GetString("username") != "iris" { + err.Status(iris.StatusForbidden).Reason(forbiddenMsg) + } + + scope.Complete(err) + }) + + failHandlerFunc := func(ctx *iris.Context) { + ctx.Set("username", "wrong") + ctx.Write("This should not be sent to the client.") + + ctx.Next() // in order to execute the next handler, which is a wrapper of transaction + } + + successHandlerFunc := func(ctx *iris.Context) { + ctx.Set("username", "iris") + ctx.Write("Hello!") + + ctx.Next() + } + + // per route after transaction(middleware) + api := iris.New() + api.Get("/transaction_after_route_middleware_fail_because_of_request_scope_fails", failHandlerFunc, transaction.ToMiddleware()) // after per route + + api.Get("/transaction_after_route_middleware_success_so_response_should_be_sent_to_the_client", successHandlerFunc, transaction.ToMiddleware()) // after per route + + e := httptest.New(api, t) + e.GET("/transaction_after_route_middleware_fail_because_of_request_scope_fails"). + Expect(). + Status(iris.StatusForbidden). + Body(). + Equal(forbiddenMsg) + + e.GET("/transaction_after_route_middleware_success_so_response_should_be_sent_to_the_client"). + Expect(). + Status(iris.StatusOK). + Body(). + Equal(allowMsg) + + // global, after all route's handlers + api = iris.New() + + api.DoneTransaction(transaction) + api.Get("/failed_because_of_done_transaction", failHandlerFunc) + + api.Get("/succeed_because_of_done_transaction", successHandlerFunc) + + e = httptest.New(api, t) + e.GET("/failed_because_of_done_transaction"). + Expect(). + Status(iris.StatusForbidden). + Body(). + Equal(forbiddenMsg) + + e.GET("/succeed_because_of_done_transaction"). + Expect(). + Status(iris.StatusOK). + Body(). + Equal(allowMsg) + + // global, before all route's handlers transaction, this is not so useful so these transaction will be succesfuly and just adds a message + api = iris.New() + transactionHTMLResponse := "Transaction here" + expectedResponse := transactionHTMLResponse + allowMsg + api.UseTransaction(func(scope *iris.TransactionScope) { + scope.Context.HTML(iris.StatusOK, transactionHTMLResponse) + // scope.Context.Next() is automatically called on UseTransaction + }) + + api.Get("/route1", func(ctx *iris.Context) { + ctx.Write(allowMsg) + }) + + e = httptest.New(api, t) + e.GET("/route1"). + Expect(). + Status(iris.StatusOK). + ContentType("text/html", api.Config.Charset). + Body(). + Equal(expectedResponse) + } diff --git a/iris.go b/iris.go index e9d749d6..1b81ae3e 100644 --- a/iris.go +++ b/iris.go @@ -80,7 +80,7 @@ const ( // IsLongTermSupport flag is true when the below version number is a long-term-support version IsLongTermSupport = false // Version is the current version number of the Iris web framework - Version = "5.1.1" + Version = "5.1.2" banner = ` _____ _ |_ _| (_) @@ -1152,12 +1152,15 @@ type ( MuxAPI interface { Party(string, ...HandlerFunc) MuxAPI // middleware serial, appending - Use(...Handler) - UseFunc(...HandlerFunc) + Use(...Handler) MuxAPI + UseFunc(...HandlerFunc) MuxAPI // returns itself, because at the most-cases used like .Layout, at the first-line party's declaration Done(...Handler) MuxAPI DoneFunc(...HandlerFunc) MuxAPI - // + + // transactions + UseTransaction(...TransactionFunc) MuxAPI + DoneTransaction(...TransactionFunc) MuxAPI // main handlers Handle(string, string, ...Handler) RouteNameFunc @@ -1236,13 +1239,13 @@ func (api *muxAPI) Party(relativePath string, handlersFn ...HandlerFunc) MuxAPI } // Use registers Handler middleware -func Use(handlers ...Handler) { - Default.Use(handlers...) +func Use(handlers ...Handler) MuxAPI { + return Default.Use(handlers...) } // UseFunc registers HandlerFunc middleware -func UseFunc(handlersFn ...HandlerFunc) { - Default.UseFunc(handlersFn...) +func UseFunc(handlersFn ...HandlerFunc) MuxAPI { + return Default.UseFunc(handlersFn...) } // Done registers Handler 'middleware' the only difference from .Use is that it @@ -1262,13 +1265,16 @@ func DoneFunc(handlersFn ...HandlerFunc) MuxAPI { } // Use registers Handler middleware -func (api *muxAPI) Use(handlers ...Handler) { +// returns itself +func (api *muxAPI) Use(handlers ...Handler) MuxAPI { api.middleware = append(api.middleware, handlers...) + return api } // UseFunc registers HandlerFunc middleware -func (api *muxAPI) UseFunc(handlersFn ...HandlerFunc) { - api.Use(convertToHandlers(handlersFn)...) +// returns itself +func (api *muxAPI) UseFunc(handlersFn ...HandlerFunc) MuxAPI { + return api.Use(convertToHandlers(handlersFn)...) } // Done registers Handler 'middleware' the only difference from .Use is that it @@ -1296,6 +1302,60 @@ func (api *muxAPI) DoneFunc(handlersFn ...HandlerFunc) MuxAPI { return api.Done(convertToHandlers(handlersFn)...) } +// UseTransaction adds transaction(s) middleware +// the difference from manually adding them to the ctx.BeginTransaction +// is that if a transaction is requested scope and is failed then the (next) handler is not executed. +// +// Returns itself. +// +// See https://github.com/iris-contrib/examples/tree/master/transactions to manually add transactions +// and https://github.com/kataras/iris/blob/master/context_test.go for more +func UseTransaction(pipes ...TransactionFunc) MuxAPI { + return Default.UseTransaction(pipes...) +} + +// UseTransaction adds transaction(s) middleware +// the difference from manually adding them to the ctx.BeginTransaction +// is that if a transaction is requested scope and is failed then the (next) handler is not executed. +// +// Returns itself. +// +// See https://github.com/iris-contrib/examples/tree/master/transactions to manually add transactions +// and https://github.com/kataras/iris/blob/master/context_test.go for more +func (api *muxAPI) UseTransaction(pipes ...TransactionFunc) MuxAPI { + return api.UseFunc(func(ctx *Context) { + for i := range pipes { + ctx.BeginTransaction(pipes[i]) + if ctx.TransactionsSkipped() { + ctx.StopExecution() + } + } + ctx.Next() + }) +} + +// DoneTransaction registers Transaction 'middleware' the only difference from .UseTransaction is that +// is executed always last, after all of each route's handlers, returns itself. +// +// See https://github.com/iris-contrib/examples/tree/master/transactions to manually add transactions +// and https://github.com/kataras/iris/blob/master/context_test.go for more +func DoneTransaction(pipes ...TransactionFunc) MuxAPI { + return Default.DoneTransaction(pipes...) +} + +// DoneTransaction registers Transaction 'middleware' the only difference from .UseTransaction is that +// is executed always last, after all of each route's handlers, returns itself. +// +// See https://github.com/iris-contrib/examples/tree/master/transactions to manually add transactions +// and https://github.com/kataras/iris/blob/master/context_test.go for more +func (api *muxAPI) DoneTransaction(pipes ...TransactionFunc) MuxAPI { + return api.DoneFunc(func(ctx *Context) { + for i := range pipes { + ctx.BeginTransaction(pipes[i]) + } + }) +} + // Handle registers a route to the server's router // if empty method is passed then registers handler(s) for all methods, same as .Any, but returns nil as result func Handle(method string, registedPath string, handlers ...Handler) RouteNameFunc {