From 37c766fef748f2c48586e30a032c6354772fd7c1 Mon Sep 17 00:00:00 2001
From: "Gerasimos (Makis) Maropoulos" <kataras2006@hotmail.com>
Date: Thu, 24 Feb 2022 23:49:46 +0200
Subject: [PATCH] new x/errors package to handle HTTP wire errors

---
 HISTORY.md                                    |   2 +
 _examples/README.md                           |   1 +
 .../http-server/listen-addr-public/main.go    |   1 +
 _examples/routing/http-errors/main.go         |   1 +
 _examples/routing/http-wire-errors/main.go    | 110 +++++++
 x/client/client.go                            |  18 +-
 x/errors/aliases.go                           |  25 ++
 x/errors/errors.go                            | 309 ++++++++++++++++++
 x/errors/validation_error.go                  | 181 ++++++++++
 9 files changed, 646 insertions(+), 2 deletions(-)
 create mode 100644 _examples/routing/http-wire-errors/main.go
 create mode 100644 x/errors/aliases.go
 create mode 100644 x/errors/errors.go
 create mode 100644 x/errors/validation_error.go

diff --git a/HISTORY.md b/HISTORY.md
index 4c9b55d8..d8ca2a57 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -28,6 +28,8 @@ The codebase for Dependency Injection, Internationalization and localization and
 
 ## Fixes and Improvements
 
+- New [x/errors](x/errors) sub-package, helps with HTTP Wire Errors. Example can be found [here](_examples/routing/http-wire-errors/main.go).
+
 - New [x/timex](x/timex) sub-package, helps working with weekdays.
 
 - Minor improvements to the [JSON Kitchen Time](x/jsonx/kitchen_time.go).
diff --git a/_examples/README.md b/_examples/README.md
index 5a563f69..0db92a9d 100644
--- a/_examples/README.md
+++ b/_examples/README.md
@@ -44,6 +44,7 @@
     * [Overview](routing/overview/main.go)
     * [Basic](routing/basic/main.go)
     * [Custom HTTP Errors](routing/http-errors/main.go)
+    * [HTTP Wire Errors](routing/http-wire-errors/main.go) **NEW**
     * [Not Found - Intelligence](routing/intelligence/main.go)
         * [Not Found - Suggest Closest Paths](routing/intelligence/manual/main.go)
     * [Dynamic Path](routing/dynamic-path/main.go)
diff --git a/_examples/http-server/listen-addr-public/main.go b/_examples/http-server/listen-addr-public/main.go
index 1e17cd0e..ceb823c9 100644
--- a/_examples/http-server/listen-addr-public/main.go
+++ b/_examples/http-server/listen-addr-public/main.go
@@ -28,6 +28,7 @@ func main() {
 					{
 						Name: "MyApp",
 						Addr: ":8080",
+						Hostname: "your-custom-sub-domain.ngrok.io", // optionally
 					},
 				},
 			},
diff --git a/_examples/routing/http-errors/main.go b/_examples/routing/http-errors/main.go
index e3fdca01..f5af0147 100644
--- a/_examples/routing/http-errors/main.go
+++ b/_examples/routing/http-errors/main.go
@@ -4,6 +4,7 @@ import (
 	"github.com/kataras/iris/v12"
 )
 
+// See _examples/routing/http-wire-errors as well.
 func main() {
 	app := iris.New()
 
diff --git a/_examples/routing/http-wire-errors/main.go b/_examples/routing/http-wire-errors/main.go
new file mode 100644
index 00000000..912e7f4f
--- /dev/null
+++ b/_examples/routing/http-wire-errors/main.go
@@ -0,0 +1,110 @@
+package main
+
+import (
+	"github.com/kataras/iris/v12"
+	// IMPORTANT, import this sub-package.
+	// Note tht it does NOT break compatibility with the
+	// standard "errors" package as the New,
+	// Is, As, Unwrap functions are aliases to the standard package.
+	"github.com/kataras/iris/v12/x/errors"
+)
+
+// Optionally, register custom error codes.
+//
+// The default list of error code names:
+// errors.Cancelled
+// errors.Unknown
+// errors.InvalidArgument
+// errors.DeadlineExceeded
+// errors.NotFound
+// errors.AlreadyExists
+// errors.PermissionDenied
+// errors.Unauthenticated
+// errors.ResourceExhausted
+// errors.FailedPrecondition
+// errors.Aborted
+// errors.OutOfRange
+// errors.Unimplemented
+// errors.Internal
+// errors.Unavailable
+// errors.DataLoss
+var (
+	Custom = errors.E("CUSTOM_CANONICAL_ERROR_NAME", iris.StatusBadRequest)
+)
+
+func main() {
+	app := iris.New()
+
+	// Custom error code name.
+	app.Get("/custom", fireCustomErrorCodeName)
+
+	// Send a simple 400 request with message and an error
+	// or with more details and data.
+	app.Post("/invalid_argument", fireInvalidArgument)
+
+	// Compatibility with the iris.Problem type (and any other custom type).
+	app.Get("/problem", fireErrorWithProblem)
+
+	app.Listen(":8080")
+}
+
+func fireCustomErrorCodeName(ctx iris.Context) {
+	Custom.Details(ctx, "message", "details with arguments: %s", "an argument")
+}
+
+func fireInvalidArgument(ctx iris.Context) {
+	var req = struct {
+		Username string `json:"username"`
+	}{}
+	if err := ctx.ReadJSON(&req); err != nil {
+		errors.InvalidArgument.Err(ctx, err)
+		return
+	}
+
+	ctx.WriteString(req.Username)
+
+	// Other examples: errors.InvalidArgument/NotFound/Internal and e.t.c.
+	// .Message(ctx, "message %s", "optional argument")
+	// .Details(ctx, "message", "details %s", "optional details argument")
+	// .Data(ctx, "message", anyTypeOfValue)
+	// .DataWithDetails(ctx, "unable to read the body", "malformed json", iris.Map{"custom": "data of any type"})
+	// .Log(ctx, "message %s", "optional argument")
+	// .LogErr(ctx, err)
+}
+
+func fireErrorWithProblem(ctx iris.Context) {
+	myCondition := true
+	if myCondition {
+		problem := iris.NewProblem().
+			// The type URI, if relative it automatically convert to absolute.
+			Type("/product-error").
+			// The title, if empty then it gets it from the status code.
+			Title("Product validation problem").
+			// Any optional details.
+			Detail("details about the product error").
+			// The status error code of the problem, can be optional here.
+			// Status(iris.StatusBadRequest).
+			// Any custom key-value pair.
+			Key("product_name", "the product name")
+
+		errors.InvalidArgument.Data(ctx, "unable to process the request", problem)
+		return
+
+		/* Prints to the client:
+		{
+		  "http_error_code": {
+		    "canonical_name": "INVALID_ARGUMENT",
+		    "status": 400
+		  },
+		  "message": "unable to process the request",
+		  "data": {
+		    "detail": "details about the product error",
+		    "product_name": "the product name",
+		    "title": "Product validation problem",
+		    "type": "/product-error"
+		  }
+		}
+		*/
+	}
+
+}
diff --git a/x/client/client.go b/x/client/client.go
index 562d8871..299b9b3c 100644
--- a/x/client/client.go
+++ b/x/client/client.go
@@ -33,6 +33,9 @@ type Client struct {
 
 	// Optional handlers that are being fired before and after each new request.
 	requestHandlers []RequestHandler
+
+	// store it here for future use.
+	keepAlive bool
 }
 
 // New returns a new Iris HTTP Client.
@@ -57,6 +60,10 @@ func New(opts ...Option) *Client {
 		opt(c)
 	}
 
+	if transport, ok := c.HTTPClient.Transport.(*http.Transport); ok {
+		c.keepAlive = !transport.DisableKeepAlives
+	}
+
 	return c
 }
 
@@ -271,6 +278,13 @@ func (c *Client) Do(ctx context.Context, method, urlpath string, payload interfa
 	return resp, respErr
 }
 
+// DrainResponseBody drains response body and close it, allowing the transport to reuse TCP connections.
+// It's automatically called on Client.ReadXXX methods on the end.
+func (c *Client) DrainResponseBody(resp *http.Response) {
+	_, _ = io.Copy(ioutil.Discard, resp.Body)
+	resp.Body.Close()
+}
+
 const (
 	acceptKey                 = "Accept"
 	contentTypeKey            = "Content-Type"
@@ -377,7 +391,7 @@ func (c *Client) ReadJSON(ctx context.Context, dest interface{}, method, urlpath
 	if err != nil {
 		return err
 	}
-	defer resp.Body.Close()
+	defer c.DrainResponseBody(resp)
 
 	if resp.StatusCode >= http.StatusBadRequest {
 		return ExtractError(resp)
@@ -398,7 +412,7 @@ func (c *Client) ReadPlain(ctx context.Context, dest interface{}, method, urlpat
 	if err != nil {
 		return err
 	}
-	defer resp.Body.Close()
+	defer c.DrainResponseBody(resp)
 
 	if resp.StatusCode >= http.StatusBadRequest {
 		return ExtractError(resp)
diff --git a/x/errors/aliases.go b/x/errors/aliases.go
new file mode 100644
index 00000000..29e732d4
--- /dev/null
+++ b/x/errors/aliases.go
@@ -0,0 +1,25 @@
+package errors
+
+import (
+	"errors"
+	"fmt"
+)
+
+var (
+	// Is is an alias of the standard errors.Is function.
+	Is = errors.Is
+	// As is an alias of the standard errors.As function.
+	As = errors.As
+	// New is an alias of the standard errors.New function.
+	New = errors.New
+	// Unwrap is an alias of the standard errors.Unwrap function.
+	Unwrap = errors.Unwrap
+)
+
+func sprintf(format string, args ...interface{}) string {
+	if len(args) > 0 {
+		return fmt.Sprintf(format, args...)
+	}
+
+	return format
+}
diff --git a/x/errors/errors.go b/x/errors/errors.go
new file mode 100644
index 00000000..99429e01
--- /dev/null
+++ b/x/errors/errors.go
@@ -0,0 +1,309 @@
+package errors
+
+import (
+	"encoding/json"
+	"fmt"
+
+	"github.com/kataras/iris/v12"
+	"github.com/kataras/iris/v12/x/client"
+)
+
+// LogErrorFunc is an alias of a function type which accepts the Iris request context and an error
+// and it's fired whenever an error should be logged.
+//
+// See "OnErrorLog" variable to change the way an error is logged,
+// by default the error is logged using the Application's Logger's Error method.
+type LogErrorFunc = func(ctx iris.Context, err error)
+
+// LogError can be modified to customize the way an error is logged to the server (most common: internal server errors, database errors et.c.).
+// Can be used to customize the error logging, e.g. using Sentry (cloud-based error console).
+var LogError LogErrorFunc = func(ctx iris.Context, err error) {
+	ctx.Application().Logger().Error(err)
+}
+
+// SkipCanceled is a package-level setting which by default
+// skips the logging of a canceled response or operation.
+// See the "Context.IsCanceled()" method and "iris.IsCanceled()" function
+// that decide if the error is caused by a canceled operation.
+//
+// Change of this setting MUST be done on initialization of the program.
+var SkipCanceled = true
+
+type (
+	// ErrorCodeName is a custom string type represents canonical error names.
+	//
+	// It contains functionality for safe and easy error populating.
+	// See its "Message", "Details", "Data" and "Log" methods.
+	ErrorCodeName string
+
+	// ErrorCode represents the JSON form ErrorCode of the Error.
+	ErrorCode struct {
+		CanonicalName ErrorCodeName `json:"canonical_name" yaml:"CanonicalName"`
+		Status        int           `json:"status" yaml:"Status"`
+	}
+)
+
+// A read-only map of valid http error codes.
+var errorCodeMap = make(map[ErrorCodeName]ErrorCode)
+
+// E registers a custom HTTP Error and returns its canonical name for future use.
+// The method "New" is reserved and was kept as it is for compatibility
+// with the standard errors package, therefore the "E" name was chosen instead.
+// The key stroke "e" is near and accessible while typing the "errors" word
+// so developers may find it easy to use.
+//
+// See "RegisterErrorCode" and "RegisterErrorCodeMap" for alternatives.
+//
+// Example:
+// 	var (
+//    NotFound = errors.E("NOT_FOUND", iris.StatusNotFound)
+// 	)
+// 	...
+// 	NotFound.Details(ctx, "resource not found", "user with id: %q was not found", userID)
+//
+// This method MUST be called on initialization, before HTTP server starts as
+// the internal map is not protected by mutex.
+func E(httpErrorCanonicalName string, httpStatusCode int) ErrorCodeName {
+	canonicalName := ErrorCodeName(httpErrorCanonicalName)
+	RegisterErrorCode(canonicalName, httpStatusCode)
+	return canonicalName
+}
+
+// RegisterErrorCode registers a custom HTTP Error.
+//
+// This method MUST be called on initialization, before HTTP server starts as
+// the internal map is not protected by mutex.
+func RegisterErrorCode(canonicalName ErrorCodeName, httpStatusCode int) {
+	errorCodeMap[canonicalName] = ErrorCode{
+		CanonicalName: canonicalName,
+		Status:        httpStatusCode,
+	}
+}
+
+// RegisterErrorCodeMap registers one or more custom HTTP Errors.
+//
+// This method MUST be called on initialization, before HTTP server starts as
+// the internal map is not protected by mutex.
+func RegisterErrorCodeMap(errorMap map[ErrorCodeName]int) {
+	if len(errorMap) == 0 {
+		return
+	}
+
+	for canonicalName, httpStatusCode := range errorMap {
+		RegisterErrorCode(canonicalName, httpStatusCode)
+	}
+}
+
+// List of default error codes a server should follow and send back to the client.
+var (
+	Cancelled          ErrorCodeName = E("CANCELLED", iris.StatusTokenRequired)
+	Unknown            ErrorCodeName = E("UNKNOWN", iris.StatusInternalServerError)
+	InvalidArgument    ErrorCodeName = E("INVALID_ARGUMENT", iris.StatusBadRequest)
+	DeadlineExceeded   ErrorCodeName = E("DEADLINE_EXCEEDED", iris.StatusGatewayTimeout)
+	NotFound           ErrorCodeName = E("NOT_FOUND", iris.StatusNotFound)
+	AlreadyExists      ErrorCodeName = E("ALREADY_EXISTS", iris.StatusConflict)
+	PermissionDenied   ErrorCodeName = E("PERMISSION_DENIED", iris.StatusForbidden)
+	Unauthenticated    ErrorCodeName = E("UNAUTHENTICATED", iris.StatusUnauthorized)
+	ResourceExhausted  ErrorCodeName = E("RESOURCE_EXHAUSTED", iris.StatusTooManyRequests)
+	FailedPrecondition ErrorCodeName = E("FAILED_PRECONDITION", iris.StatusBadRequest)
+	Aborted            ErrorCodeName = E("ABORTED", iris.StatusConflict)
+	OutOfRange         ErrorCodeName = E("OUT_OF_RANGE", iris.StatusBadRequest)
+	Unimplemented      ErrorCodeName = E("UNIMPLEMENTED", iris.StatusNotImplemented)
+	Internal           ErrorCodeName = E("INTERNAL", iris.StatusInternalServerError)
+	Unavailable        ErrorCodeName = E("UNAVAILABLE", iris.StatusServiceUnavailable)
+	DataLoss           ErrorCodeName = E("DATA_LOSS", iris.StatusInternalServerError)
+)
+
+// Message sends an error with a simple message to the client.
+func (e ErrorCodeName) Message(ctx iris.Context, format string, args ...interface{}) {
+	fail(ctx, e, sprintf(format, args...), "", nil, nil)
+}
+
+// Details sends an error with a message and details to the client.
+func (e ErrorCodeName) Details(ctx iris.Context, msg, details string, detailsArgs ...interface{}) {
+	fail(ctx, e, msg, sprintf(details, detailsArgs...), nil, nil)
+}
+
+// Data sends an error with a message and json data to the client.
+func (e ErrorCodeName) Data(ctx iris.Context, msg string, data interface{}) {
+	fail(ctx, e, msg, "", nil, data)
+}
+
+// DataWithDetails sends an error with a message, details and json data to the client.
+func (e ErrorCodeName) DataWithDetails(ctx iris.Context, msg, details string, data interface{}) {
+	fail(ctx, e, msg, details, nil, data)
+}
+
+// Validation sends an error which contains the invalid fields to the client.
+func (e ErrorCodeName) Validation(ctx iris.Context, errs ...ValidationError) {
+	fail(ctx, e, "validation failure", "fields were invalid", errs, nil)
+}
+
+// Err sends the error's text as a message to the client.
+// In exception, if the given "err" is a type of validation error
+// then the Validation method is called instead.
+func (e ErrorCodeName) Err(ctx iris.Context, err error) {
+	if err == nil {
+		return
+	}
+
+	if validationErrors, ok := AsValidationErrors(err); ok {
+		e.Validation(ctx, validationErrors...)
+		return
+	}
+
+	e.Message(ctx, err.Error())
+}
+
+// Log sends an error of "format" and optional "args" to the client and prints that
+// error using the "LogError" package-level function, which can be customized.
+//
+// See "LogErr" too.
+func (e ErrorCodeName) Log(ctx iris.Context, format string, args ...interface{}) {
+	if SkipCanceled {
+		if ctx.IsCanceled() {
+			return
+		}
+
+		for _, arg := range args {
+			if err, ok := arg.(error); ok {
+				if iris.IsErrCanceled(err) {
+					return
+				}
+			}
+		}
+	}
+
+	err := fmt.Errorf(format, args...)
+	e.LogErr(ctx, err)
+}
+
+// LogErr sends the given "err" as message to the client and prints that
+// error to using the "LogError" package-level function, which can be customized.
+func (e ErrorCodeName) LogErr(ctx iris.Context, err error) {
+	if SkipCanceled && (ctx.IsCanceled() || iris.IsErrCanceled(err)) {
+		return
+	}
+
+	LogError(ctx, err)
+
+	e.Message(ctx, "server error")
+}
+
+// HandleAPIError handles remote server errors.
+// Optionally, use it when you write your server's HTTP clients using the the /x/client package.
+// When the HTTP Client sends data to a remote server but that remote server
+// failed to accept the request as expected, then the error will be proxied
+// to this server's end-client.
+//
+// When the given "err" is not a type of client.APIError then
+// the error will be sent using the "Internal.LogErr" method which sends
+// HTTP internal server error to the end-client and
+// prints the "err" using the "LogError" package-level function.
+func HandleAPIError(ctx iris.Context, err error) {
+	// Error expected and came from the external server,
+	// save its body so we can forward it to the end-client.
+	if apiErr, ok := client.GetError(err); ok {
+		statusCode := apiErr.Response.StatusCode
+		if statusCode >= 400 && statusCode < 500 {
+			InvalidArgument.DataWithDetails(ctx, "remote server error", "invalid client request", apiErr.Body)
+		} else {
+			Internal.Data(ctx, "remote server error", apiErr.Body)
+		}
+
+		// Unavailable.DataWithDetails(ctx, "remote server error", "unavailable", apiErr.Body)
+		return
+	}
+
+	Internal.LogErr(ctx, err)
+}
+
+var (
+	// ErrUnexpected is the HTTP error which sent to the client
+	// when server fails to send an error, it's a fallback error.
+	// The server fails to send an error on two cases:
+	// 1. when the provided error code name is not registered (the error value is the ErrUnexpectedErrorCode)
+	// 2. when the error contains data but cannot be encoded to json (the value of the error is the result error of json.Marshal).
+	ErrUnexpected = E("UNEXPECTED_ERROR", iris.StatusInternalServerError)
+	// ErrUnexpectedErrorCode is the error which logged
+	// when the given error code name is not registered.
+	ErrUnexpectedErrorCode = New("unexpected error code name")
+)
+
+// Error represents the JSON form of "http wire errors".
+type Error struct {
+	ErrorCode        ErrorCode        `json:"http_error_code" yaml:"HTTPErrorCode"`
+	Message          string           `json:"message,omitempty" yaml:"Message"`
+	Details          string           `json:"details,omitempty" yaml:"Details"`
+	ValidationErrors ValidationErrors `json:"validation,omitempty" yaml:"Validation,omitempty"`
+	Data             json.RawMessage  `json:"data,omitempty" yaml:"Data,omitempty"` // any other custom json data.
+}
+
+// Error method completes the error interface. It just returns the canonical name, status code, message and details.
+func (err Error) Error() string {
+	if err.Message == "" {
+		err.Message = "<empty>"
+	}
+
+	if err.Details == "" {
+		err.Details = "<empty>"
+	}
+
+	if err.ErrorCode.CanonicalName == "" {
+		err.ErrorCode.CanonicalName = ErrUnexpected
+	}
+
+	if err.ErrorCode.Status <= 0 {
+		err.ErrorCode.Status = iris.StatusInternalServerError
+	}
+
+	return sprintf("iris http wire error: canonical name: %s, http status code: %d, message: %s, details: %s", err.ErrorCode.CanonicalName, err.ErrorCode.Status, err.Message, err.Details)
+}
+
+func fail(ctx iris.Context, codeName ErrorCodeName, msg, details string, validationErrors ValidationErrors, dataValue interface{}) {
+	errorCode, ok := errorCodeMap[codeName]
+	if !ok {
+		// This SHOULD NEVER happen, all ErrorCodeNames MUST be registered.
+		LogError(ctx, ErrUnexpectedErrorCode)
+		fail(ctx, ErrUnexpected, msg, details, validationErrors, dataValue)
+		return
+	}
+
+	var data json.RawMessage
+	if dataValue != nil {
+		switch v := dataValue.(type) {
+		case json.RawMessage:
+			data = v
+		case []byte:
+			data = v
+		case error:
+			if msg == "" {
+				msg = v.Error()
+			} else if details == "" {
+				details = v.Error()
+			} else {
+				data = json.RawMessage(v.Error())
+			}
+		default:
+			b, err := json.Marshal(v)
+			if err != nil {
+				LogError(ctx, err)
+				fail(ctx, ErrUnexpected, err.Error(), "", nil, nil)
+				return
+			}
+			data = b
+
+		}
+	}
+
+	err := Error{
+		ErrorCode:        errorCode,
+		Message:          msg,
+		Details:          details,
+		Data:             data,
+		ValidationErrors: validationErrors,
+	}
+
+	// ctx.SetErr(err)
+	ctx.StopWithJSON(errorCode.Status, err)
+}
diff --git a/x/errors/validation_error.go b/x/errors/validation_error.go
new file mode 100644
index 00000000..49b0b998
--- /dev/null
+++ b/x/errors/validation_error.go
@@ -0,0 +1,181 @@
+package errors
+
+import (
+	"math"
+	"regexp"
+	"strconv"
+	"strings"
+)
+
+// AsValidationErrors reports wheether the given "err" is a type of validation error(s).
+func AsValidationErrors(err error) (ValidationErrors, bool) {
+	if err == nil {
+		return nil, false
+	}
+
+	switch e := err.(type) {
+	case ValidationError:
+		return ValidationErrors{e}, true
+	case ValidationErrors:
+		return e, true
+	case *ValidationErrors:
+		return *e, true
+	default:
+		return nil, false
+	}
+}
+
+// ValueValidator is a generic interface which can be used to check if the value is valid for insert (or for comparison inside another validation step).
+// Useful for enums.
+// Should return a non-empty string on validation error, that string is the failure reason.
+type ValueValidator interface {
+	Validate() string
+}
+
+// ValidationError describes a field validation error.
+type ValidationError struct {
+	Field  string      `json:"field" yaml:"Field"`
+	Value  interface{} `json:"value" yaml:"Value"`
+	Reason string      `json:"reason" yaml:"Reason"`
+}
+
+// Error completes the standard error interface.
+func (e ValidationError) Error() string {
+	return sprintf("field %q got invalid value of %v: reason: %s", e.Field, e.Value, e.Reason)
+}
+
+// ValidationErrors is just a custom type of ValidationError slice.
+type ValidationErrors []ValidationError
+
+// Error completes the error interface.
+func (e ValidationErrors) Error() string {
+	var buf strings.Builder
+	for i, err := range e {
+		buf.WriteByte('[')
+		buf.WriteString(strconv.Itoa(i))
+		buf.WriteByte(']')
+		buf.WriteByte(' ')
+
+		buf.WriteString(err.Error())
+
+		if i < len(e)-1 {
+			buf.WriteByte(',')
+			buf.WriteByte(' ')
+		}
+	}
+
+	return buf.String()
+}
+
+// Is reports whether the given "err" is a type of validation error or validation errors.
+func (e ValidationErrors) Is(err error) bool {
+	if err == nil {
+		return false
+	}
+
+	switch err.(type) {
+	case ValidationError:
+		return true
+	case *ValidationError:
+		return true
+	case ValidationErrors:
+		return true
+	case *ValidationErrors:
+		return true
+	default:
+		return false
+	}
+}
+
+// Add is a helper for appending a validation error.
+func (e *ValidationErrors) Add(err ValidationError) *ValidationErrors {
+	if err.Field == "" || err.Reason == "" {
+		return e
+	}
+
+	*e = append(*e, err)
+	return e
+}
+
+// Join joins an existing Errors to this errors list.
+func (e *ValidationErrors) Join(errs ValidationErrors) *ValidationErrors {
+	*e = append(*e, errs...)
+	return e
+}
+
+// Validate returns the result of the value's Validate method, if exists otherwise
+// it adds the field and value to the error list and reports false (invalidated).
+// If reason is empty, means that the field is valid, this method will return true.
+func (e *ValidationErrors) Validate(field string, value interface{}) bool {
+	var reason string
+
+	if v, ok := value.(ValueValidator); ok {
+		reason = v.Validate()
+	}
+
+	if reason != "" {
+		e.Add(ValidationError{
+			Field:  field,
+			Value:  value,
+			Reason: reason,
+		})
+
+		return false
+	}
+
+	return true
+}
+
+// MustBeSatisfiedFunc compares the value with the given "isEqualFunc" function and reports
+// if it's valid or not. If it's not valid, a new ValidationError is added to the "e" list.
+func (e *ValidationErrors) MustBeSatisfiedFunc(field string, value string, isEqualFunc func(string) bool) bool {
+	if !isEqualFunc(value) {
+		e.Add(ValidationError{
+			Field:  field,
+			Value:  value,
+			Reason: "failed to satisfy constraint",
+		})
+		return false
+	}
+
+	return true
+}
+
+// MustBeSatisfied compares the value with the given regex and reports
+// if it's valid or not. If it's not valid, a new ValidationError is added to the "e" list.
+func (e *ValidationErrors) MustBeSatisfied(field string, value string, regex *regexp.Regexp) bool {
+	return e.MustBeSatisfiedFunc(field, value, regex.MatchString)
+}
+
+// MustBeNotEmptyString reports and fails if the given "value" is empty.
+func (e *ValidationErrors) MustBeNotEmptyString(field string, value string) bool {
+	if strings.TrimSpace(value) == "" {
+		e.Add(ValidationError{
+			Field:  field,
+			Value:  value,
+			Reason: "must be not an empty string",
+		})
+
+		return false
+	}
+
+	return true
+}
+
+// MustBeInRangeString reports whether the "value" is in range of min and max.
+func (e *ValidationErrors) MustBeInRangeString(field string, value string, minIncluding, maxIncluding int) bool {
+	if maxIncluding <= 0 {
+		maxIncluding = math.MaxInt32
+	}
+
+	if len(value) < minIncluding || len(value) > maxIncluding {
+		e.Add(ValidationError{
+			Field:  field,
+			Value:  value,
+			Reason: sprintf("characters length must be between %d and %d", minIncluding, maxIncluding),
+		})
+		return false
+	}
+
+	return true
+}