Skip to content

Commit

Permalink
feat: added service errors + generic pg repo support
Browse files Browse the repository at this point in the history
  • Loading branch information
Adam Bezecny committed Oct 31, 2023
1 parent 6905dc3 commit a7fce16
Show file tree
Hide file tree
Showing 7 changed files with 472 additions and 1 deletion.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@

Library holding common helper code shared by different backend applications.

**IMPORTANT:** strictly no business logic here! This is public repo so that it is retrievable as any other open source library!
**IMPORTANT:** strictly no business logic here (no sensitive values like AWS account numbers, ARNs, etc.)! This is public repo so that it is retrievable as any other open source library!
33 changes: 33 additions & 0 deletions adapter/repository/postgres/postgres.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package postgres

import (
"context"
"github.com/jackc/pgx/v5/pgxpool"
)

// DB represents access (via underlying pool object) to PostgreSQL database
type DB struct {
*pgxpool.Pool
}

// NewPostgresDBFromUri creates new postgres connection from provided uri
func NewPostgresDBFromUri(ctx context.Context, uri string) (*DB, error) {
db, err := pgxpool.New(ctx, uri)
if err != nil {
return nil, err
}

err = db.Ping(ctx)
if err != nil {
return nil, err
}

return &DB{
db,
}, nil
}

// Close closes the database connection
func (db *DB) Close() {
db.Pool.Close()
}
77 changes: 77 additions & 0 deletions errors/rest_api_error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package errors

import (
"github.com/gin-gonic/gin"
"github.com/rotisserie/eris"
"net/http"
)

// ApiError is generic structure used to return error from REST API
type ApiError struct {
Error string `json:"error"`
Detail string `json:"detail"`
}

// TranslateServiceErrorToAPIError holds mapping between protocol agnostic
// service errors and rest api specific errors with http status codes
func TranslateServiceErrorToAPIError(ctx *gin.Context, err error, includeDetails bool) {
var target1 *ServiceErrorUnauthorized
if eris.As(err, &target1) {
ReturnUnauthorizedError(ctx, err, includeDetails)
return
}

var target2 *ServiceErrorForbidden
if eris.As(err, &target2) {
ReturnForbiddenError(ctx, err, includeDetails)
return
}

var target3 *ServiceErrorNotFound
if eris.As(err, &target3) {
ReturnNotFoundError(ctx, err, includeDetails)
return
}

var target4 *ServiceErrorNotImplemented
if eris.As(err, &target4) {
ReturnNotImplementedError(ctx, err, includeDetails)
return
}

ReturnInternalServerError(ctx, err, includeDetails)
}

func getGinH(err error, includeDetails bool) any {
if includeDetails {
detail := eris.ToString(err, true)

return gin.H{
"error": err.Error(),
"detail": detail,
}
} else {
return gin.H{"error": err.Error()}
}
}

func ReturnInternalServerError(c *gin.Context, err error, includeDetails bool) {
c.JSON(http.StatusInternalServerError, getGinH(err, includeDetails))
}
func ReturnBadRequestError(c *gin.Context, err error, includeDetails bool) {
c.JSON(http.StatusBadRequest, getGinH(err, includeDetails))
}
func ReturnNotImplementedError(c *gin.Context, err error, includeDetails bool) {
c.JSON(http.StatusNotImplemented, getGinH(err, includeDetails))
}
func ReturnNotFoundError(c *gin.Context, err error, includeDetails bool) {
c.JSON(http.StatusNotFound, getGinH(err, includeDetails))
}

func ReturnUnauthorizedError(c *gin.Context, err error, includeDetails bool) {
c.JSON(http.StatusUnauthorized, getGinH(err, includeDetails))
}

func ReturnForbiddenError(c *gin.Context, err error, includeDetails bool) {
c.JSON(http.StatusForbidden, getGinH(err, includeDetails))
}
119 changes: 119 additions & 0 deletions errors/rest_api_error_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package errors

import (
errorHelper "errors"
"fmt"
"github.com/gin-gonic/gin"
"github.com/rotisserie/eris"
"github.com/stretchr/testify/assert"
"net/http"
"net/http/httptest"
"testing"
)

const (
includeErrorDetails = true
)

func TestTranslateToHttpError404_nil_nil(t *testing.T) {
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)

serviceError := NewServiceErrorNotFound(nil, "")

errStr := eris.ToString(serviceError, true)
fmt.Println(errStr)
/*
SERVICE_ERROR_NOT_FOUND
errors.TestTranslateToHttpError404_nil_nil:/Users/adambezecny/dev/hrnogomet-backend-kit/errors/rest_api_error_test.go:23
errors.NewServiceErrorNotFound:/Users/adambezecny/dev/hrnogomet-backend-kit/errors/service_error.go:142
errors.newServiceError:/Users/adambezecny/dev/hrnogomet-backend-kit/errors/service_error.go:121
SERVICE_ERROR_NOT_FOUND
*/

TranslateServiceErrorToAPIError(c, serviceError, includeErrorDetails)
assert.Equal(t, http.StatusNotFound, w.Code)
}

func TestTranslateToHttpError404_x_nil(t *testing.T) {
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)

serviceError := NewServiceErrorNotFound(errorHelper.New("fromBackend404"), "")

errStr := eris.ToString(serviceError, true)
fmt.Println(errStr)

/*
SERVICE_ERROR_NOT_FOUND
errors.TestTranslateToHttpError404_x_nil:/Users/adambezecny/dev/hrnogomet-backend-kit/errors/rest_api_error_test.go:44
errors.NewServiceErrorNotFound:/Users/adambezecny/dev/hrnogomet-backend-kit/errors/service_error.go:142
errors.newServiceError:/Users/adambezecny/dev/hrnogomet-backend-kit/errors/service_error.go:125
fromBackend404
*/

TranslateServiceErrorToAPIError(c, serviceError, includeErrorDetails)
assert.Equal(t, http.StatusNotFound, w.Code)
}

func TestTranslateToHttpError404_nil_x(t *testing.T) {
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)

serviceError := NewServiceErrorNotFound(nil, "custom404")

errStr := eris.ToString(serviceError, true)
fmt.Println(errStr)
/*
custom404
errors.TestTranslateToHttpError404_nil_x:/Users/adambezecny/dev/hrnogomet-backend-kit/errors/rest_api_error_test.go:66
errors.NewServiceErrorNotFound:/Users/adambezecny/dev/hrnogomet-backend-kit/errors/service_error.go:142
errors.newServiceError:/Users/adambezecny/dev/hrnogomet-backend-kit/errors/service_error.go:129
SERVICE_ERROR_NOT_FOUND
*/

TranslateServiceErrorToAPIError(c, serviceError, includeErrorDetails)
assert.Equal(t, http.StatusNotFound, w.Code)
}

func TestTranslateToHttpError404_x_x(t *testing.T) {
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)

serviceError := NewServiceErrorNotFound(errorHelper.New("fromBackend404"), "custom404")

errStr := eris.ToString(serviceError, true)
fmt.Println(errStr)
/*
custom404
errors.TestTranslateToHttpError404_x_x:/Users/adambezecny/dev/hrnogomet-backend-kit/errors/rest_api_error_test.go:87
errors.NewServiceErrorNotFound:/Users/adambezecny/dev/hrnogomet-backend-kit/errors/service_error.go:142
errors.newServiceError:/Users/adambezecny/dev/hrnogomet-backend-kit/errors/service_error.go:133
fromBackend404
*/

TranslateServiceErrorToAPIError(c, serviceError, includeErrorDetails)
assert.Equal(t, http.StatusNotFound, w.Code)
}

func TestTranslateToHttpErrorNormalError(t *testing.T) {
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)

serviceError := errorHelper.New("something bad happened")

errStr := eris.ToString(serviceError, true)
fmt.Println(errStr)

/*
something bad happened
*/

TranslateServiceErrorToAPIError(c, serviceError, includeErrorDetails)
assert.Equal(t, http.StatusInternalServerError, w.Code)
}
158 changes: 158 additions & 0 deletions errors/service_error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package errors

import "github.com/rotisserie/eris"

// ServiceError represents protocol agnostic business error that might be raised within
// service or repository. This error than must be translated at handler level to protocol
// specific error, e.g. HTTP status code with response payload in case of REST API
// Default implementation for REST API translation see ApiError and TranslateServiceErrorToAPIError
type ServiceError interface {
SetErrorText(text string)
SetNestedError(err error)
Error() string // ServiceError is also Error, i.e. implementors of ServiceError implement implicitly also Error interface
}

type ServiceErrorUnauthorized struct {
ErrorText string
NestedError error
}

func (e *ServiceErrorUnauthorized) Error() string {
return e.ErrorText
}

func (e *ServiceErrorUnauthorized) SetErrorText(text string) {
e.ErrorText = text
}

func (e *ServiceErrorUnauthorized) SetNestedError(err error) {
e.NestedError = err
}

type ServiceErrorNotFound struct {
ErrorText string
NestedError error
}

func (e *ServiceErrorNotFound) Error() string {
return e.ErrorText
}

func (e *ServiceErrorNotFound) SetErrorText(text string) {
e.ErrorText = text
}

func (e *ServiceErrorNotFound) SetNestedError(err error) {
e.NestedError = err
}

type ServiceErrorForbidden struct {
ErrorText string
NestedError error
}

func (e *ServiceErrorForbidden) Error() string {
return e.ErrorText
}

func (e *ServiceErrorForbidden) SetErrorText(text string) {
e.ErrorText = text
}

func (e *ServiceErrorForbidden) SetNestedError(err error) {
e.NestedError = err
}

type ServiceErrorBadRequest struct {
ErrorText string
NestedError error
}

func (e *ServiceErrorBadRequest) Error() string {
return e.ErrorText
}

func (e *ServiceErrorBadRequest) SetErrorText(text string) {
e.ErrorText = text
}

func (e *ServiceErrorBadRequest) SetNestedError(err error) {
e.NestedError = err
}

type ServiceErrorNotImplemented struct {
ErrorText string
NestedError error
}

func (e *ServiceErrorNotImplemented) Error() string {
return e.ErrorText
}

func (e *ServiceErrorNotImplemented) SetErrorText(text string) {
e.ErrorText = text
}

func (e *ServiceErrorNotImplemented) SetNestedError(err error) {
e.NestedError = err
}

type ServiceErrorInternalServerError struct {
ErrorText string
NestedError error
}

func (e *ServiceErrorInternalServerError) SetErrorText(text string) {
e.ErrorText = text
}

func (e *ServiceErrorInternalServerError) SetNestedError(err error) {
e.NestedError = err
}

func (e *ServiceErrorInternalServerError) Error() string {
return e.ErrorText
}

func newServiceError(nestedBackendError error, customMessage string, defaultMessage string, specificServiceError ServiceError) error {
if nestedBackendError == nil && customMessage == "" {
specificServiceError.SetErrorText(defaultMessage)
specificServiceError.SetNestedError(nil)
return eris.Wrap(specificServiceError, defaultMessage)
} else if nestedBackendError != nil && customMessage == "" {
specificServiceError.SetErrorText(nestedBackendError.Error())
specificServiceError.SetNestedError(nestedBackendError)
return eris.Wrap(specificServiceError, defaultMessage)
} else if nestedBackendError == nil && customMessage != "" {
specificServiceError.SetErrorText(defaultMessage)
specificServiceError.SetNestedError(nil)
return eris.Wrap(specificServiceError, customMessage)
} else /* nestedBackendError != nil && customMessage != "" */ {
specificServiceError.SetErrorText(nestedBackendError.Error())
specificServiceError.SetNestedError(nestedBackendError)
return eris.Wrap(specificServiceError, customMessage)
}
}

func NewServiceErrorInternalServerError(nestedBackendError error, customMessage string) error {
return newServiceError(nestedBackendError, customMessage, "SERVICE_ERROR_INTERNAL_SERVER_ERROR", &ServiceErrorInternalServerError{})
}

func NewServiceErrorNotFound(nestedBackendError error, customMessage string) error {
return newServiceError(nestedBackendError, customMessage, "SERVICE_ERROR_NOT_FOUND", &ServiceErrorNotFound{})
}
func NewServiceErrorBadRequest(nestedBackendError error, customMessage string) error {
return newServiceError(nestedBackendError, customMessage, "SERVICE_ERROR_BAD_REQUEST", &ServiceErrorBadRequest{})
}

func NewServiceErrorNotImplemented(nestedBackendError error, customMessage string) error {
return newServiceError(nestedBackendError, customMessage, "SERVICE_ERROR_NOT_IMPLEMENTED", &ServiceErrorNotImplemented{})
}

func NewServiceErrorUnauthorized(nestedBackendError error, customMessage string) error {
return newServiceError(nestedBackendError, customMessage, "SERVICE_ERROR_UNAUTHORIZED", &ServiceErrorUnauthorized{})
}

func NewServiceErrorForbidden(nestedBackendError error, customMessage string) error {
return newServiceError(nestedBackendError, customMessage, "SERVICE_ERROR_FORBIDDEN", &ServiceErrorForbidden{})
}
Loading

0 comments on commit a7fce16

Please sign in to comment.