Skip to content

Commit

Permalink
feat: Implement error handling
Browse files Browse the repository at this point in the history
  • Loading branch information
MaikelVeen committed Sep 6, 2024
1 parent f05de16 commit f23832e
Show file tree
Hide file tree
Showing 12 changed files with 194 additions and 34 deletions.
11 changes: 11 additions & 0 deletions api/v1/error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package v1

import "net/http"

var ErrorCodeMapping = map[ErrorCode]int{
ParameterInvalid: http.StatusBadRequest,
ParameterMissing: http.StatusBadRequest,
ProcessingError: http.StatusInternalServerError,
ResourceAlreadyExists: http.StatusConflict,
ResourceMissing: http.StatusNotFound,
}
4 changes: 2 additions & 2 deletions api/v1/item.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import (
"github.com/glass-cms/glasscms/parser"
)

// MapToDomain converts an api.Item to an item.Item.
func (i *ItemCreate) MapToDomain() *item.Item {
// ToItem converts an api.ItemCreate to an item.Item.
func (i *ItemCreate) ToItem() *item.Item {
if i == nil {
return nil
}
Expand Down
2 changes: 1 addition & 1 deletion api/v1/item_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ func TestItem_MapToDomain(t *testing.T) {
Properties: tt.fields.Properties,
UpdateTime: tt.fields.UpdateTime,
}
got := apiItem.MapToDomain()
got := apiItem.ToItem()

// Adjust for potential differences in time (e.g., slight differences due to test execution timing)
if !got.CreateTime.IsZero() && !tt.want.CreateTime.IsZero() {
Expand Down
21 changes: 0 additions & 21 deletions item/item.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,3 @@ type Item struct {
Properties map[string]any `mapstructure:"properties"`
Metadata map[string]any `mapstructure:"metadata"`
}

// New creates a new Item instance with the given parameters.
func New(
name string,
displayName string,
content string,
createTime time.Time,
updateTime time.Time,
properties map[string]any,
metadata map[string]any,
) *Item {
return &Item{
Name: name,
DisplayName: displayName,
Content: content,
CreateTime: createTime,
UpdateTime: updateTime,
Properties: properties,
Metadata: metadata,
}
}
6 changes: 3 additions & 3 deletions lib/resource/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package resource

// AlreadyExistsError represents an error when a resource already exists.
type AlreadyExistsError struct {
UID string
Name string
Resource string
err error
}
Expand All @@ -12,9 +12,9 @@ func (e *AlreadyExistsError) Error() string {
}

// NewAlreadyExistsError creates a new ResourceAlreadyExistsError.
func NewAlreadyExistsError(uid, resource string, err error) *AlreadyExistsError {
func NewAlreadyExistsError(name, resource string, err error) *AlreadyExistsError {
return &AlreadyExistsError{
UID: uid,
Name: name,
Resource: resource,
err: err,
}
Expand Down
18 changes: 18 additions & 0 deletions server/handler/json.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package handler

import (
"encoding/json"
"net/http"

"github.com/glass-cms/glasscms/lib/mediatype"
)

// RespondWithJSON writes a JSON response to the response.
func RespondWithJSON[T any](w http.ResponseWriter, statusCode int, data T) {
w.Header().Set("Content-Type", mediatype.ApplicationJSON)
w.WriteHeader(statusCode)

if err := json.NewEncoder(w).Encode(data); err != nil {
http.Error(w, "Failed to encode JSON", http.StatusInternalServerError)
}
}
49 changes: 49 additions & 0 deletions server/handler/v1/error_handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package v1

import (
"net/http"
"reflect"

v1 "github.com/glass-cms/glasscms/api/v1"
"github.com/glass-cms/glasscms/server/handler"
)

// ErrorMapper is a function that maps an error to an API error response.
type ErrorMapper func(error) *v1.Error

type ErrorHandler struct {
Mappers map[reflect.Type]ErrorMapper
}

// NewErrorHandler returns a new instance of ErrorHandler.
func NewErrorHandler() *ErrorHandler {
return &ErrorHandler{
Mappers: make(map[reflect.Type]ErrorMapper),
}
}

// RegisterErrorMapper registers an error mapper for a specific error type.
func (h *ErrorHandler) RegisterErrorMapper(errType reflect.Type, mapper ErrorMapper) {
h.Mappers[errType] = mapper
}

// HandleError handles an error by writing an appropriate response to the client.
func (h *ErrorHandler) HandleError(w http.ResponseWriter, _ *http.Request, err error) {
if err == nil {
return
}

errType := reflect.TypeOf(err)
if mapper, exists := h.Mappers[errType]; exists {
errResp := mapper(err)

statusCode, ok := v1.ErrorCodeMapping[errResp.Code]
if !ok {
statusCode = http.StatusInternalServerError
}

handler.RespondWithJSON(w, statusCode, errResp)
}

// TODO: Default error handling.
}
1 change: 1 addition & 0 deletions server/handler/v1/error_handler_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package v1_test
27 changes: 27 additions & 0 deletions server/handler/v1/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package v1

import (
"errors"
"fmt"

v1 "github.com/glass-cms/glasscms/api/v1"
"github.com/glass-cms/glasscms/lib/resource"
)

// ErrorMapperAlreadyExistsError maps a resource.AlreadyExistsError to an API error response.
func ErrorMapperAlreadyExistsError(err error) *v1.Error {
var alreadyExistsErr *resource.AlreadyExistsError
if !errors.As(err, &alreadyExistsErr) {
panic("error is not a resource.AlreadyExistsError")
}

return &v1.Error{
Code: v1.ResourceAlreadyExists,
Message: fmt.Sprintf("An %s with the name already exists", alreadyExistsErr.Resource),
Type: v1.ApiError,
Details: map[string]interface{}{
"resource": alreadyExistsErr.Resource,
"name": alreadyExistsErr.Name,
},
}
}
59 changes: 59 additions & 0 deletions server/handler/v1/errors_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package v1_test

import (
"errors"
"testing"

v1 "github.com/glass-cms/glasscms/api/v1"
"github.com/glass-cms/glasscms/lib/resource"
v1_handler "github.com/glass-cms/glasscms/server/handler/v1"
"github.com/stretchr/testify/require"
)

func TestErrorMapperAlreadyExistsError(t *testing.T) {
t.Parallel()

type args struct {
err error
}
tests := map[string]struct {
args args
want *v1.Error
expectPanics bool
}{
"maps resource.AlreadyExistsError to an API error response": {
args: args{
err: resource.NewAlreadyExistsError("item1", "item", errors.New("underlying error")),
},
want: &v1.Error{
Code: v1.ResourceAlreadyExists,
Message: "An item with the name already exists",
Type: v1.ApiError,
Details: map[string]interface{}{
"resource": "item",
"name": "item1",
},
},
},
"panics if error is not a resource.AlreadyExistsError": {
args: args{
err: errors.New("some error"),
},
expectPanics: true,
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
t.Parallel()

if tt.expectPanics {
require.Panics(t, func() {
v1_handler.ErrorMapperAlreadyExistsError(tt.args.err)
})
return
}

require.Equal(t, tt.want, v1_handler.ErrorMapperAlreadyExistsError(tt.args.err))
})
}
}
25 changes: 20 additions & 5 deletions server/handler/v1/handler_v1.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,19 @@ package v1
import (
"log/slog"
"net/http"
"reflect"

v1 "github.com/glass-cms/glasscms/api/v1"
"github.com/glass-cms/glasscms/item"
"github.com/glass-cms/glasscms/lib/resource"
"github.com/glass-cms/glasscms/server/handler"
)

type APIHandler struct {
logger *slog.Logger
itemService *item.Service

errorHandler *ErrorHandler
}

// NewAPIHandler returns a new instance of ApiHandler.
Expand All @@ -21,8 +25,9 @@ func NewAPIHandler(
service *item.Service,
) *APIHandler {
return &APIHandler{
logger: logger,
itemService: service,
logger: logger,
itemService: service,
errorHandler: NewErrorHandler(),
}
}

Expand All @@ -36,12 +41,22 @@ func (s *APIHandler) Handler(
convertedMiddlewares[i] = v1.MiddlewareFunc(mw)
}

s.registerErrorMappers()

return v1.HandlerWithOptions(s, v1.StdHTTPServerOptions{
BaseURL: "/v1",
BaseRouter: baseRouter,
Middlewares: convertedMiddlewares,
BaseURL: "/v1",
BaseRouter: baseRouter,
Middlewares: convertedMiddlewares,
ErrorHandlerFunc: s.errorHandler.HandleError,
})
}

func (s *APIHandler) registerErrorMappers() {
s.errorHandler.RegisterErrorMapper(
reflect.TypeOf(&resource.AlreadyExistsError{}),
ErrorMapperAlreadyExistsError,
)
}

var _ v1.ServerInterface = (*APIHandler)(nil)
var _ handler.VersionedHandler = (*APIHandler)(nil)
5 changes: 3 additions & 2 deletions server/handler/v1/item.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@ func (s *APIHandler) ItemsCreate(w http.ResponseWriter, r *http.Request) {
return
}

if err = s.itemService.CreateItem(ctx, request.MapToDomain()); err != nil {
err = s.itemService.CreateItem(ctx, request.ToItem())
if err != nil {
s.logger.ErrorContext(ctx, fmt.Errorf("failed to create item: %w", err).Error())
w.WriteHeader(http.StatusInternalServerError)
s.errorHandler.HandleError(w, r, err)
return
}

Expand Down

0 comments on commit f23832e

Please sign in to comment.