From 65980d33635766d6309494790e720c4732375f9b Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Thu, 15 Dec 2016 15:14:48 +0200 Subject: [PATCH] New Feature: Request-Scoped Transactions Example: https://github.com/iris-contrib/examples/tree/master/request_transactions --- HISTORY.md | 4 ++ README.md | 7 +-- context.go | 111 ++++++++++++++++++++++++++++++++++++++++++++++++ context_test.go | 64 ++++++++++++++++++++++++++++ iris.go | 2 +- 5 files changed, 184 insertions(+), 4 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index a5c6e885..a52c4e15 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -2,6 +2,10 @@ **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.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). + ## 5.0.3 -> 5.0.4 diff --git a/README.md b/README.md index cef98940..0e157fee 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@
-Releases +Releases Examples @@ -52,6 +52,7 @@ Feature Overview - Automatically install and serve certificates from https://letsencrypt.org - Robust routing and middleware ecosystem - Build RESTful APIs +- Request-Scoped Transactions - Group API's and subdomains with wildcard support - Body binding for JSON, XML, Forms, can be extended to use your own custom binders - More than 50 handy functions to send HTTP responses @@ -919,7 +920,7 @@ I recommend writing your API tests using this new library, [httpexpect](https:// Versioning ------------ -Current: **v5.0.4** +Current: **v5.1.0** Stable: **[v4 LTS](https://github.com/kataras/iris/tree/4.0.0#versioning)** @@ -928,7 +929,7 @@ Todo ------------ - [ ] Server-side React render, as requested [here](https://github.com/kataras/iris/issues/503) - +- [+] [NEW: Request-Scoped Transactions](https://github.com/iris-contrib/examples/tree/master/request_transactions) 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)! People diff --git a/context.go b/context.go index a00c4984..afd9b12f 100644 --- a/context.go +++ b/context.go @@ -1121,6 +1121,117 @@ 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 { + // failure status code, required + statusCode int + // plain text message, optional + message string // if it's empty then the already registered custom(or default) http error will be fired. +} + +// Status sets the http status code of this error +func (err *ErrWithStatus) Status(statusCode int) *ErrWithStatus { + err.statusCode = statusCode + return err +} + +// Reason sets the reason message of this error +func (err *ErrWithStatus) Reason(msg string) *ErrWithStatus { + err.message = msg + return err +} + +// AppendReason just appends a reason message +func (err *ErrWithStatus) AppendReason(msg string) *ErrWithStatus { + err.message += "\n" + msg + return err +} + +// Error implements the error standard +func (err ErrWithStatus) Error() string { + return err.message +} + +// NewErrWithStatus returns an new custom error type which should be used +// side by side with Transaction(s) +func NewErrWithStatus() *ErrWithStatus { + return new(ErrWithStatus) +} + +// 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. +// +// The error can be a type of ErrWithStatus, create using the iris.NewErrWithStatus(). +func (r *RequestTransactionScope) Complete(err error) { + if err != nil && err.Error() != "" { + ctx := r.Context + // 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: + cType := string(ctx.Response.Header.ContentType()) + if cType == "" { + cType = "text/plain; charset=" + ctx.framework.Config.Charset + } + + // clears: + // - body + // - cookies + // - any headers + // 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) + return + } + +} + +// 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)) { + // 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} + pipe(scope) // run the context inside its scope + *ctx = *scope.Context // copy back the context +} + // Log logs to the iris defined logger func (ctx *Context) Log(format string, a ...interface{}) { ctx.framework.Logger.Printf(format, a...) diff --git a/context_test.go b/context_test.go index be67e6df..002dfed3 100644 --- a/context_test.go +++ b/context_test.go @@ -780,3 +780,67 @@ func TestTemplatesDisabled(t *testing.T) { e := httptest.New(iris.Default, t) e.GET("/renderErr").Expect().Status(iris.StatusServiceUnavailable).Body().Equal(expctedErrMsg) } + +func TestRequestTransactions(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) { + // OPTIONAL STEP: + // create a new custom type of error here to keep track of the status code and reason message + err := iris.NewErrWithStatus() + + // we should use scope.Context if we want to rollback on any errors lives inside this function clojure. + // if you want persistence then use the 'ctx'. + scope.Context.Text(iris.StatusOK, "Blablabla this should not be sent to the client because we will fill the err with a message and status") + + // var firstErr error = do this() // your code here + // var secondErr error = try_do_this() // your code here + // var thirdErr error = try_do_this() // your code here + // var fail bool = false + + // if firstErr != nil || secondErr != nil || thirdErr != nil { + // fail = true + // } + // or err.AppendReason(firstErr.Error()) // ... err.Reason(dbErr.Error()).Status(500) + + // virtualize a fake error for the proof of concept + fail := true + + 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) + } + + // OPTIONAl STEP: + // but useful if we want to post back an error message to the client if the transaction failed. + // 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() + }) + + // 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) + }) + + e := httptest.New(iris.Default, t) + + e.GET("/failFirsTransactionButSuccessSecond"). + Expect(). + Status(iris.StatusOK). + ContentType("text/html", iris.Config.Charset). + Body(). + Equal(expectedTestMessage) +} diff --git a/iris.go b/iris.go index c8f968de..796a6f7e 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.0.4" + Version = "5.1.0" banner = ` _____ _ |_ _| (_)