New Feature: Request-Scoped Transactions

Example:
https://github.com/iris-contrib/examples/tree/master/request_transactions
This commit is contained in:
Gerasimos (Makis) Maropoulos 2016-12-15 15:14:48 +02:00
parent 1ff949b357
commit 65980d3363
5 changed files with 184 additions and 4 deletions

View File

@ -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`. **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 ## 5.0.3 -> 5.0.4

View File

@ -20,7 +20,7 @@
<br/> <br/>
<a href="https://github.com/kataras/iris/releases"><img src="https://img.shields.io/badge/%20version%20-%205.0.4%20-blue.svg?style=flat-square" alt="Releases"></a> <a href="https://github.com/kataras/iris/releases"><img src="https://img.shields.io/badge/%20version%20-%205.1.0%20-blue.svg?style=flat-square" alt="Releases"></a>
<a href="https://github.com/iris-contrib/examples"><img src="https://img.shields.io/badge/%20examples-repository-3362c2.svg?style=flat-square" alt="Examples"></a> <a href="https://github.com/iris-contrib/examples"><img src="https://img.shields.io/badge/%20examples-repository-3362c2.svg?style=flat-square" alt="Examples"></a>
@ -52,6 +52,7 @@ Feature Overview
- Automatically install and serve certificates from https://letsencrypt.org - Automatically install and serve certificates from https://letsencrypt.org
- Robust routing and middleware ecosystem - Robust routing and middleware ecosystem
- Build RESTful APIs - Build RESTful APIs
- Request-Scoped Transactions
- Group API's and subdomains with wildcard support - Group API's and subdomains with wildcard support
- Body binding for JSON, XML, Forms, can be extended to use your own custom binders - Body binding for JSON, XML, Forms, can be extended to use your own custom binders
- More than 50 handy functions to send HTTP responses - 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 Versioning
------------ ------------
Current: **v5.0.4** Current: **v5.1.0**
Stable: **[v4 LTS](https://github.com/kataras/iris/tree/4.0.0#versioning)** 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) - [ ] 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)! 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 People

View File

@ -1121,6 +1121,117 @@ func (ctx *Context) MaxAge() int64 {
return -1 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 // Log logs to the iris defined logger
func (ctx *Context) Log(format string, a ...interface{}) { func (ctx *Context) Log(format string, a ...interface{}) {
ctx.framework.Logger.Printf(format, a...) ctx.framework.Logger.Printf(format, a...)

View File

@ -780,3 +780,67 @@ func TestTemplatesDisabled(t *testing.T) {
e := httptest.New(iris.Default, t) e := httptest.New(iris.Default, t)
e.GET("/renderErr").Expect().Status(iris.StatusServiceUnavailable).Body().Equal(expctedErrMsg) e.GET("/renderErr").Expect().Status(iris.StatusServiceUnavailable).Body().Equal(expctedErrMsg)
} }
func TestRequestTransactions(t *testing.T) {
iris.ResetDefault()
firstTransictionFailureMessage := "Error: Virtual failure!!!"
secondTransictionSuccessHTMLMessage := "<h1>This will sent at all cases because it lives on different transaction and it doesn't fails</h1>"
persistMessage := "<h1>I persist show this message to the client!</h1>"
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)
}

View File

@ -80,7 +80,7 @@ const (
// IsLongTermSupport flag is true when the below version number is a long-term-support version // IsLongTermSupport flag is true when the below version number is a long-term-support version
IsLongTermSupport = false IsLongTermSupport = false
// Version is the current version number of the Iris web framework // Version is the current version number of the Iris web framework
Version = "5.0.4" Version = "5.1.0"
banner = ` _____ _ banner = ` _____ _
|_ _| (_) |_ _| (_)