From 48e770dab0f6e132c23f3941196d11cdb491b5e2 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Thu, 15 Dec 2016 17:16:17 +0200 Subject: [PATCH] Update to 5.1.1 - Addons for the last feature, Transaction scopes. Read HISTORY.md Read HISTORY.md and example here: github.com/iris-contrib/examples/tree/master/transactions --- HISTORY.md | 9 +++- README.md | 6 +-- context.go | 138 +++++++++++++++++++++++++++++++++++------------- context_test.go | 58 +++++++++++++++----- iris.go | 2 +- 5 files changed, 158 insertions(+), 55 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index a52c4e15..0605aa6c 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -2,9 +2,16 @@ **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.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)`. + +Note: `RequestTransactionScope` renamed to ,simply, `TransactionScope`. + ## 5.0.4 -> 5.1.0 -- **NEW (UNIQUE?) FEATURE**: Request-scoped transactions inside handler's context. Proof-of-concept example [here](https://github.com/iris-contrib/examples/tree/master/request_transactions). +- **NEW (UNIQUE?) FEATURE**: Request-scoped transactions inside handler's context. Proof-of-concept example [here](https://github.com/iris-contrib/examples/tree/master/transactions). ## 5.0.3 -> 5.0.4 diff --git a/README.md b/README.md index 63b9ddcb..0dcf44d1 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.0** +Current: **v5.1.1** Stable: **[v4 LTS](https://github.com/kataras/iris/tree/4.0.0#versioning)** @@ -929,7 +929,7 @@ Todo ------------ - [ ] Server-side React render, as requested [here](https://github.com/kataras/iris/issues/503) -- [x] [v5.1.0: Request-Scoped Transactions](https://github.com/iris-contrib/examples/tree/master/request_transactions), simple and elegant. +- [x] [v5.1.0: (Request) Scoped Transactions](https://github.com/iris-contrib/examples/tree/master/transactions), simple and elegant. Iris is a **Community-Driven** Project, waiting for your suggestions and [feature requests](https://github.com/kataras/iris/issues?utf8=%E2%9C%93&q=label%3A%22feature%20request%22)! diff --git a/context.go b/context.go index afd9b12f..9fc6737d 100644 --- a/context.go +++ b/context.go @@ -1121,20 +1121,6 @@ func (ctx *Context) MaxAge() int64 { return -1 } -// RequestTransactionScope 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). -// But I can provide you a simple example here: https://github.com/iris-contrib/examples/tree/master/request_transactions -// -// Note that this is unique and new -// (=I haver never seen any other examples or code in Golang on this subject, so far, as with the most of iris features...) -// it's not covers all paths, -// such as databases, this should be managed by the libraries you use to make your database connection, -// this transaction scope is only for iris' request/response(Context). -type RequestTransactionScope struct { - Context *Context -} - // ErrWithStatus custom error type which is useful // to send an error containing the http status code and a reason type ErrWithStatus struct { @@ -1145,6 +1131,8 @@ type ErrWithStatus struct { } // Status sets the http status code of this error +// if only status exists but no reason then +// custom http error of this staus (if any) will be fired (context.EmitError) func (err *ErrWithStatus) Status(statusCode int) *ErrWithStatus { err.statusCode = statusCode return err @@ -1173,15 +1161,77 @@ 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). +// But I can provide you a simple example here: https://github.com/iris-contrib/examples/tree/master/transactions +// +// Note that this is unique and new +// (=I haver never seen any other examples or code in Golang on this subject, so far, as with the most of iris features...) +// it's not covers all paths, +// such as databases, this should be managed by the libraries you use to make your database connection, +// this transaction scope is only for iris' request & response(Context). +type TransactionScope struct { + Context *Context + isRequestScoped bool + isFailure bool +} + +// RequestScoped receives a boolean which determinates if other transactions depends on this. +// If setted true then whenever this transaction is not completed succesfuly, +// the rest of the transactions will be not executed at all. +// +// Defaults to false, execute all transactions on their own independently scopes. +func (r *TransactionScope) RequestScoped(isRequestScoped bool) { + r.isRequestScoped = isRequestScoped +} + // Complete completes the transaction -// - if the error is not nil then the response -// is resetting and sends an error to the client. -// - if the error is nil then the response sent as expected. +// rollback and send an error when: +// 1. a not nil error AND non-empty reason AND custom type error has status code +// 2. a not nil error AND empty reason BUT custom type error has status code +// 3. a not nil error AND non-empty reason. // // The error can be a type of ErrWithStatus, create using the iris.NewErrWithStatus(). -func (r *RequestTransactionScope) Complete(err error) { - if err != nil && err.Error() != "" { +func (r *TransactionScope) Complete(err error) { + if err != nil { + ctx := r.Context + statusCode := StatusInternalServerError // default http status code if not provided + reason := err.Error() + + if errWstatus, ok := err.(*ErrWithStatus); ok { + if errWstatus.statusCode > 0 { + // get the status code from the custom error type + statusCode = errWstatus.statusCode + + // empty error message but status code given, + if reason == "" { + r.isFailure = true + // reset everything, cookies and headers and body. + ctx.Response.Reset() + // execute from custom (if any) http error (template or plain text) + ctx.EmitError(errWstatus.statusCode) + return + } + } + } else 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 + } + + // rollback and send an error when we have: + // 1. a not nil error AND non-empty reason AND custom type error has status code + // 2. a not nil error AND empty reason BUT custom type error has status code + // 3. a not nil error AND non-empty reason. + // reset any previous response, // except the content type we may use it to fire an error or take that from custom error type (?) // no let's keep the custom error type as simple as possible, take that from prev attempt: @@ -1197,39 +1247,53 @@ func (r *RequestTransactionScope) Complete(err error) { // and anything else we tried to sent before. ctx.Response.Reset() - statusCode := StatusInternalServerError // default http status code if not provided - reason := err.Error() - shouldFireCustom := false - if errWstatus, ok := err.(*ErrWithStatus); ok { - statusCode = errWstatus.statusCode - if reason == "" { // if we have custom error type with a given status but empty reason then fire from custom http error (or default) - shouldFireCustom = true - } - } - if shouldFireCustom { - // if it's not our custom type of error nor an error with a non empty reason then we fire a default - // or custom (EmitError) using the 500 internal server error - ctx.EmitError(StatusInternalServerError) - return - } // fire from the error or the custom error type ctx.SetStatusCode(statusCode) ctx.SetContentType(cType) ctx.SetBodyString(reason) + r.isFailure = true return } } +// skipTransactionsContextKey set this to any value to stop executing next transactions +// it's a context-key in order to be used from anywhere, set it by calling the SkipTransactions() +const skipTransactionsContextKey = "@@IRIS_TRANSACTIONS_SKIP_@@" + +// SkipTransactions if called then skip the rest of the transactions +// or all of them if called before the first transaction +func (ctx *Context) SkipTransactions() { + ctx.Set(skipTransactionsContextKey, 1) +} + +// TransactionsSkipped returns true if the transactions skipped or canceled at all. +func (ctx *Context) TransactionsSkipped() bool { + if n, err := ctx.GetInt(skipTransactionsContextKey); err == nil && n == 1 { + return true + } + return false +} + // BeginTransaction starts a request scoped transaction. // -// See more here: https://github.com/iris-contrib/examples/tree/master/request_transactions -func (ctx *Context) BeginTransaction(pipe func(scope *RequestTransactionScope)) { +// See more here: https://github.com/iris-contrib/examples/tree/master/transactions +func (ctx *Context) BeginTransaction(pipe func(scope *TransactionScope)) { + // do NOT begin a transaction when the previous transaction has been failed + // and it was requested scoped or SkipTransactions called manually + if ctx.TransactionsSkipped() { + return + } // not the best way but this should be do the job if we want multiple transaction in the same handler and context. tempCtx := *ctx // clone the temp context - scope := &RequestTransactionScope{Context: &tempCtx} + scope := &TransactionScope{Context: &tempCtx} pipe(scope) // run the context inside its scope *ctx = *scope.Context // copy back the context + + if scope.isRequestScoped && scope.isFailure { + ctx.SkipTransactions() + } + } // Log logs to the iris defined logger diff --git a/context_test.go b/context_test.go index 002dfed3..7b12a7a0 100644 --- a/context_test.go +++ b/context_test.go @@ -781,15 +781,17 @@ func TestTemplatesDisabled(t *testing.T) { e.GET("/renderErr").Expect().Status(iris.StatusServiceUnavailable).Body().Equal(expctedErrMsg) } -func TestRequestTransactions(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

" persistMessage := "

I persist show this message to the client!

" - expectedTestMessage := firstTransictionFailureMessage + secondTransictionSuccessHTMLMessage + persistMessage - iris.Get("/failFirsTransactionButSuccessSecond", func(ctx *iris.Context) { - ctx.BeginTransaction(func(scope *iris.RequestTransactionScope) { + 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 + scope.RequestScoped(isRequestScoped) + // OPTIONAL STEP: // create a new custom type of error here to keep track of the status code and reason message err := iris.NewErrWithStatus() @@ -808,8 +810,7 @@ func TestRequestTransactions(t *testing.T) { // } // or err.AppendReason(firstErr.Error()) // ... err.Reason(dbErr.Error()).Status(500) - // virtualize a fake error for the proof of concept - fail := true + fail := shouldFail if fail { err.Status(iris.StatusInternalServerError). @@ -822,25 +823,56 @@ func TestRequestTransactions(t *testing.T) { // if the reason is empty then the transaction completed succesfuly, // otherwise we rollback the whole response body and cookies and everything lives inside the scope.Request. scope.Complete(err) - }) + } + } - ctx.BeginTransaction(func(scope *iris.RequestTransactionScope) { - scope.Context.HTML(iris.StatusOK, - secondTransictionSuccessHTMLMessage) - // * if we don't have any 'throw error' logic then no need of scope.Complete() - }) + successTransaction := func(scope *iris.TransactionScope) { + scope.Context.HTML(iris.StatusOK, + secondTransictionSuccessHTMLMessage) + // * if we don't have any 'throw error' logic then no need of scope.Complete() + } + persistMessageHandler := func(ctx *iris.Context) { // OPTIONAL, depends on the usage: // at any case, what ever happens inside the context's transactions send this to the client ctx.HTML(iris.StatusOK, persistMessage) + } + + iris.Get("/failFirsTransactionButSuccessSecondWithPersistMessage", func(ctx *iris.Context) { + ctx.BeginTransaction(maybeFailureTransaction(true, false)) + ctx.BeginTransaction(successTransaction) + persistMessageHandler(ctx) + }) + + iris.Get("/failFirsTransactionButSuccessSecond", func(ctx *iris.Context) { + ctx.BeginTransaction(maybeFailureTransaction(true, false)) + ctx.BeginTransaction(successTransaction) + }) + + iris.Get("/failAllBecauseOfRequestScopeAndFailure", func(ctx *iris.Context) { + ctx.BeginTransaction(maybeFailureTransaction(true, true)) + ctx.BeginTransaction(successTransaction) }) e := httptest.New(iris.Default, t) + e.GET("/failFirsTransactionButSuccessSecondWithPersistMessage"). + Expect(). + Status(iris.StatusOK). + ContentType("text/html", iris.Config.Charset). + Body(). + Equal(firstTransictionFailureMessage + secondTransictionSuccessHTMLMessage + persistMessage) + e.GET("/failFirsTransactionButSuccessSecond"). Expect(). Status(iris.StatusOK). ContentType("text/html", iris.Config.Charset). Body(). - Equal(expectedTestMessage) + Equal(firstTransictionFailureMessage + secondTransictionSuccessHTMLMessage) + + e.GET("/failAllBecauseOfRequestScopeAndFailure"). + Expect(). + Status(iris.StatusInternalServerError). + Body(). + Equal(firstTransictionFailureMessage) } diff --git a/iris.go b/iris.go index 796a6f7e..e9d749d6 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.0" + Version = "5.1.1" banner = ` _____ _ |_ _| (_)