From d8af2a1e14682347dd5707283909898a2ecc73d6 Mon Sep 17 00:00:00 2001
From: "Gerasimos (Makis) Maropoulos" <kataras2006@hotmail.com>
Date: Sun, 5 Jun 2022 06:15:10 +0300
Subject: [PATCH] context transactions removed and make Context.Domain
 customizable as requested

---
 HISTORY.md                                    |   2 +
 _examples/README.md                           |   1 -
 .../response-writer/transactions/main.go      |  54 ------
 context/context.go                            |  73 +------
 context/response_recorder.go                  |  11 +-
 context/transaction.go                        | 179 ------------------
 core/host/interrupt.go                        |  19 +-
 iris.go                                       |   2 +-
 8 files changed, 28 insertions(+), 313 deletions(-)
 delete mode 100644 _examples/response-writer/transactions/main.go
 delete mode 100644 context/transaction.go

diff --git a/HISTORY.md b/HISTORY.md
index 92602986..72496cec 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -28,6 +28,8 @@ The codebase for Dependency Injection, Internationalization and localization and
 
 ## Fixes and Improvements
 
+- Make `Context.Domain()` customizable by letting developers to set the custom `Context.GetDomain` package-level function.
+- Remove Request Context-based Transaction feature as its usage can be replaced with just the Iris Context (as of go1.7+) and better [project](_examples/project) structure.
 - Fix [#1882](https://github.com/kataras/iris/issues/1882)
 - Fix [#1877](https://github.com/kataras/iris/issues/1877)
 - Fix [#1876](https://github.com/kataras/iris/issues/1876)
diff --git a/_examples/README.md b/_examples/README.md
index 64eb7c9a..28821cc0 100644
--- a/_examples/README.md
+++ b/_examples/README.md
@@ -200,7 +200,6 @@
     * [Protocol Buffers](response-writer/protobuf/main.go)
     * [HTTP/2 Server Push](response-writer/http2push/main.go)
     * [Stream Writer](response-writer/stream-writer/main.go)
-    * [Transactions](response-writer/transactions/main.go)
     * [Server-Sent Events](response-writer/sse/main.go)
         * [SSE 3rd-party (r3labs/sse)](response-writer/sse-third-party/main.go)
         * [SSE 3rd-party (alexandrevicenzi/go-sse)](response-writer/sse-third-party-2/main.go)
diff --git a/_examples/response-writer/transactions/main.go b/_examples/response-writer/transactions/main.go
deleted file mode 100644
index 7b18b64d..00000000
--- a/_examples/response-writer/transactions/main.go
+++ /dev/null
@@ -1,54 +0,0 @@
-package main
-
-import (
-	"github.com/kataras/iris/v12"
-	"github.com/kataras/iris/v12/context"
-)
-
-func main() {
-	app := iris.New()
-
-	// subdomains works with all available routers, like other features too.
-
-	app.Get("/", func(ctx iris.Context) {
-		ctx.BeginTransaction(func(t *context.Transaction) {
-			// OPTIONAl STEP: , if true then the next transictions will not be executed if this transiction fails
-			// t.SetScope(context.RequestTransactionScope)
-
-			// OPTIONAL STEP:
-			// create a new custom type of error here to keep track of the status code and reason message
-			err := context.NewTransactionErrResult()
-
-			// we should use t.Context if we want to rollback on any errors lives inside this function clojure.
-			t.Context().Text("Blablabla this should not be sent to the client because we will fill the err with a message and status")
-
-			// virtualize a fake error here, for the sake of the example
-			fail := true
-			if fail {
-				err.StatusCode = iris.StatusInternalServerError
-				// NOTE: if empty reason then the default or the custom http error will be fired (like ctx.FireStatusCode)
-				err.Reason = "Error: Virtual failure!!"
-			}
-
-			// 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 successfully,
-			// otherwise we rollback the whole response writer's body,
-			// headers and cookies, status code and everything lives inside this transaction
-			t.Complete(err)
-		})
-
-		ctx.BeginTransaction(func(t *context.Transaction) {
-			t.Context().HTML("<h1>This will sent at all cases because it lives on different transaction and it doesn't fails</h1>")
-			// * if we don't have any 'throw error' logic then no need of scope.Complete()
-		})
-
-		// OPTIONALLY, depends on the usage:
-		// at any case, what ever happens inside the context's transactions send this to the client
-		ctx.HTML("<h1>Let's add a second html message to the response, " +
-			"if the transaction was failed and it was request scoped then this message would " +
-			"not been shown. But it has a transient scope(default) so, it is visible as expected!</h1>")
-	})
-
-	app.Listen(":8080")
-}
diff --git a/context/context.go b/context/context.go
index 992477dc..8117afa8 100644
--- a/context/context.go
+++ b/context/context.go
@@ -1001,7 +1001,8 @@ func (ctx *Context) Host() string {
 }
 
 // GetDomain resolves and returns the server's domain.
-func GetDomain(hostport string) string {
+// To customize its behavior, developers can modify this package-level function at initialization.
+var GetDomain = func(hostport string) string {
 	host := hostport
 	if tmp, _, err := net.SplitHostPort(hostport); err == nil {
 		host = tmp
@@ -1459,7 +1460,7 @@ func (ctx *Context) GetContentLength() int64 {
 // StatusCode sets the status code header to the response.
 // Look .GetStatusCode & .FireStatusCode too.
 //
-// Remember, the last one before .Write matters except recorder and transactions.
+// Note that you must set status code before write response body (except when recorder is used).
 func (ctx *Context) StatusCode(statusCode int) {
 	ctx.writer.WriteHeader(statusCode)
 }
@@ -5656,7 +5657,7 @@ func (ctx *Context) MaxAge() int64 {
 }
 
 //  +------------------------------------------------------------+
-//  | Advanced: Response Recorder and Transactions               |
+//  | Advanced: Response Recorder                                |
 //  +------------------------------------------------------------+
 
 // Record transforms the context's basic and direct responseWriter to a *ResponseRecorder
@@ -5691,72 +5692,6 @@ func (ctx *Context) IsRecording() (*ResponseRecorder, bool) {
 	return rr, ok
 }
 
-// ErrTransactionInterrupt can be used to manually force-complete a Context's transaction
-// and log(warn) the wrapped error's message.
-// Usage: `... return fmt.Errorf("my custom error message: %w", context.ErrTransactionInterrupt)`.
-var ErrTransactionInterrupt = errors.New("transaction interrupted")
-
-// BeginTransaction starts a scoped transaction.
-//
-// Can't say a lot here because 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, especially here).
-//
-// 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 context's response.
-// Transactions have their own middleware ecosystem also.
-//
-// See https://github.com/kataras/iris/tree/master/_examples/ for more
-func (ctx *Context) BeginTransaction(pipe func(t *Transaction)) {
-	// 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
-	}
-
-	// start recording in order to be able to control the full response writer
-	ctx.Record()
-
-	t := newTransaction(ctx) // it calls this *context, so the overriding with a new pool's New of context.Context wil not work here.
-	defer func() {
-		if err := recover(); err != nil {
-			ctx.app.Logger().Warn(fmt.Errorf("recovery from panic: %w", ErrTransactionInterrupt))
-			// complete (again or not , doesn't matters) the scope without loud
-			t.Complete(nil)
-			// we continue as normal, no need to return here*
-		}
-
-		// write the temp contents to the original writer
-		t.Context().ResponseWriter().CopyTo(ctx.writer)
-		// give back to the transaction the original writer (SetBeforeFlush works this way and only this way)
-		// this is tricky but nessecery if we want ctx.FireStatusCode to work inside transactions
-		t.Context().ResetResponseWriter(ctx.writer)
-	}()
-
-	// run the worker with its context clone inside.
-	pipe(t)
-}
-
-// 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.values.Set(skipTransactionsContextKey, 1)
-}
-
-// TransactionsSkipped returns true if the transactions skipped or canceled at all.
-func (ctx *Context) TransactionsSkipped() bool {
-	if n, err := ctx.values.GetInt(skipTransactionsContextKey); err == nil && n == 1 {
-		return true
-	}
-	return false
-}
-
 // Exec calls the framewrok's ServeHTTPC
 // based on this context but with a changed method and path
 // like it was requested by the user, but it is not.
diff --git a/context/response_recorder.go b/context/response_recorder.go
index d2162d97..ab19c7aa 100644
--- a/context/response_recorder.go
+++ b/context/response_recorder.go
@@ -29,17 +29,14 @@ func releaseResponseRecorder(w *ResponseRecorder) {
 	rrpool.Put(w)
 }
 
-// A ResponseRecorder is used mostly by context's transactions
-// in order to record and change if needed the body, status code and headers.
+// A ResponseRecorder is used mostly for testing
+// in order to record and modify, if necessary, the body and status code and headers.
 //
-// Developers are not limited to manually ask to record a response.
-// To turn on the recorder from a Handler,
-// rec := context.Recorder()
+// See `context.Recorder`` method too.
 type ResponseRecorder struct {
 	ResponseWriter
 
-	// keep track of the body in order to be
-	// resetable and useful inside custom transactions
+	// keep track of the body written.
 	chunks []byte
 	// the saved headers
 	headers http.Header
diff --git a/context/transaction.go b/context/transaction.go
deleted file mode 100644
index 823b1502..00000000
--- a/context/transaction.go
+++ /dev/null
@@ -1,179 +0,0 @@
-package context
-
-// TransactionErrResult could be named also something like 'MaybeError',
-// it is useful to send it on transaction.Complete in order to execute a custom error mesasge to the user.
-//
-// in simple words it's just a 'traveler message' between the transaction and its scope.
-// it is totally optional
-type TransactionErrResult struct {
-	StatusCode int
-	// if reason is empty then the already relative registered (custom or not)
-	// error will be executed if the scope allows that.
-	Reason      string
-	ContentType string
-}
-
-// Error returns the reason given by the user or an empty string
-func (err TransactionErrResult) Error() string {
-	return err.Reason
-}
-
-// IsFailure returns true if this is an actual error
-func (err TransactionErrResult) IsFailure() bool {
-	return StatusCodeNotSuccessful(err.StatusCode)
-}
-
-// NewTransactionErrResult returns a new transaction result with the given error message,
-// it can be empty too, but if not then the transaction's scope is decided what to do with that
-func NewTransactionErrResult() TransactionErrResult {
-	return TransactionErrResult{}
-}
-
-// TransactionScope is the manager of the transaction's response, can be resseted and skipped
-// from its parent context or execute an error or skip other transactions
-type TransactionScope interface {
-	// EndTransaction returns if can continue to the next transactions or not (false)
-	// called after Complete, empty or not empty error
-	EndTransaction(maybeErr TransactionErrResult, ctx *Context) bool
-}
-
-// TransactionScopeFunc the transaction's scope signature
-type TransactionScopeFunc func(maybeErr TransactionErrResult, ctx *Context) bool
-
-// EndTransaction ends the transaction with a callback to itself, implements the TransactionScope interface
-func (tsf TransactionScopeFunc) EndTransaction(maybeErr TransactionErrResult, ctx *Context) bool {
-	return tsf(maybeErr, ctx)
-}
-
-//  +------------------------------------------------------------+
-//  | Transaction Implementation                                 |
-//  +------------------------------------------------------------+
-
-// Transaction gives the users the opportunity to code their route handlers  cleaner and safier
-// it receives a scope which is decided when to send an error to the user, recover from panics
-// stop the execution of the next transactions and so on...
-//
-// it's default scope is the TransientTransactionScope which is silently
-// skips the current transaction's response if transaction.Complete accepts a non-empty error.
-//
-// Create and set custom transactions scopes with transaction.SetScope.
-//
-// For more information please visit the tests.
-type Transaction struct {
-	context  *Context
-	parent   *Context
-	hasError bool
-	scope    TransactionScope
-}
-
-func newTransaction(from *Context) *Transaction {
-	tempCtx := *from
-	writer := tempCtx.ResponseWriter().Clone()
-	tempCtx.ResetResponseWriter(writer)
-	t := &Transaction{
-		parent:  from,
-		context: &tempCtx,
-		scope:   TransientTransactionScope,
-	}
-
-	return t
-}
-
-// Context returns the current context of the transaction.
-func (t *Transaction) Context() *Context {
-	return t.context
-}
-
-// SetScope sets the current transaction's scope
-// iris.RequestTransactionScope || iris.TransientTransactionScope (default).
-func (t *Transaction) SetScope(scope TransactionScope) {
-	t.scope = scope
-}
-
-// Complete completes the transaction
-// rollback and send an error when the error is not empty.
-// The next steps depends on its Scope.
-//
-// The error can be a type of context.NewTransactionErrResult().
-func (t *Transaction) Complete(err error) {
-	maybeErr := TransactionErrResult{}
-
-	if err != nil {
-		t.hasError = true
-
-		statusCode := 400 // bad request
-		reason := err.Error()
-		cType := "text/plain; charset=" + t.context.Application().ConfigurationReadOnly().GetCharset()
-
-		if errWstatus, ok := err.(TransactionErrResult); ok {
-			if errWstatus.StatusCode > 0 {
-				statusCode = errWstatus.StatusCode
-			}
-
-			if errWstatus.Reason != "" {
-				reason = errWstatus.Reason
-			}
-			// get the content type used on this transaction
-			if cTypeH := t.context.GetContentType(); cTypeH != "" {
-				cType = cTypeH
-			}
-
-		}
-		maybeErr.StatusCode = statusCode
-		maybeErr.Reason = reason
-		maybeErr.ContentType = cType
-	}
-	// the transaction ends with error or not error, it decides what to do next with its Response
-	// the Response is appended to the parent context an all cases but it checks for empty body,headers and all that,
-	// if they are empty (silent error or not error at all)
-	// then all transaction's actions are skipped as expected
-	canContinue := t.scope.EndTransaction(maybeErr, t.context)
-	if !canContinue {
-		t.parent.SkipTransactions()
-	}
-}
-
-// TransientTransactionScope explanation:
-//
-// independent 'silent' scope, if transaction fails (if transaction.IsFailure() == true)
-// then its response is not written to the real context no error is provided to the user.
-// useful for the most cases.
-var TransientTransactionScope = TransactionScopeFunc(func(maybeErr TransactionErrResult, ctx *Context) bool {
-	if maybeErr.IsFailure() {
-		ctx.Recorder().Reset() // this response is skipped because it's empty.
-	}
-	return true
-})
-
-// RequestTransactionScope explanation:
-//
-// if scope fails (if transaction.IsFailure() == true)
-// then the rest of the context's response  (transaction or normal flow)
-// is not written to the client, and an error status code is written instead.
-var RequestTransactionScope = TransactionScopeFunc(func(maybeErr TransactionErrResult, ctx *Context) bool {
-	if maybeErr.IsFailure() {
-
-		// we need to register a beforeResponseFlush event here in order
-		// to execute last the FireStatusCode
-		// (which will reset the whole response's body, status code and headers set from normal flow or other transactions too)
-		ctx.ResponseWriter().SetBeforeFlush(func() {
-			// we need to re-take the context's response writer
-			// because inside here the response writer is changed to the original's
-			// look ~context:1306
-			w := ctx.ResponseWriter().(*ResponseRecorder)
-			if maybeErr.Reason != "" {
-				// send the error with the info user provided
-				w.SetBodyString(maybeErr.Reason)
-				w.WriteHeader(maybeErr.StatusCode)
-				ctx.ContentType(maybeErr.ContentType)
-			} else {
-				// else execute the registered user error and skip the next transactions and all normal flow,
-				ctx.StopWithStatus(maybeErr.StatusCode)
-			}
-		})
-
-		return false
-	}
-
-	return true
-})
diff --git a/core/host/interrupt.go b/core/host/interrupt.go
index cdd74072..07e717fe 100644
--- a/core/host/interrupt.go
+++ b/core/host/interrupt.go
@@ -7,15 +7,30 @@ import (
 	"syscall"
 )
 
-// RegisterOnInterrupt registers a global function to call when CTRL+C/CMD+C pressed or a unix kill command received.
+// RegisterOnInterrupt registers a global function to call when CTRL+C pressed or a unix kill command received.
 func RegisterOnInterrupt(cb func()) {
+	// var cb func()
+	// switch v := callbackFuncOrFuncReturnsError.(type) {
+	// case func():
+	// 	cb = v
+	// case func() error:
+	// 	cb = func() { v() }
+	// default:
+	// 	panic(fmt.Errorf("unknown type of RegisterOnInterrupt callback: expected func() or func() error but got: %T", v))
+	// }
+
 	Interrupt.Register(cb)
 }
 
 // Interrupt watches the os.Signals for interruption signals
 // and fires the callbacks when those happens.
 // A call of its `FireNow` manually will fire and reset the registered interrupt handlers.
-var Interrupt = new(interruptListener)
+var Interrupt TnterruptListener = new(interruptListener)
+
+type TnterruptListener interface {
+	Register(cb func())
+	FireNow()
+}
 
 type interruptListener struct {
 	mu   sync.Mutex
diff --git a/iris.go b/iris.go
index 697dc562..20318c3f 100644
--- a/iris.go
+++ b/iris.go
@@ -588,7 +588,7 @@ func (app *Application) NewHost(srv *http.Server) *host.Supervisor {
 	if !app.config.DisableInterruptHandler {
 		// when CTRL/CMD+C pressed.
 		shutdownTimeout := 10 * time.Second
-		host.RegisterOnInterrupt(host.ShutdownOnInterrupt(su, shutdownTimeout))
+		RegisterOnInterrupt(host.ShutdownOnInterrupt(su, shutdownTimeout))
 		// app.logger.Debugf("Host: register server shutdown on interrupt(CTRL+C/CMD+C)")
 	}