core/internal/port/handler_http_api_error.go

216 lines
4.8 KiB
Go

package port
import (
"errors"
"net/http"
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
"gitea.dwysokinski.me/twhelp/corev3/internal/port/internal/apimodel"
"github.com/ettle/strcase"
"github.com/oapi-codegen/runtime"
)
type apiError struct {
status int
code string
path []string
params map[string]any
message string
}
func (e apiError) Error() string {
return e.message
}
func (e apiError) toResponse() apimodel.ErrorResponse {
return apimodel.ErrorResponse{
Errors: []apimodel.Error{
{
Message: e.message,
Params: e.params,
Path: e.path,
Code: e.code,
},
},
}
}
type apiErrors []apiError
func (es apiErrors) status() int {
if len(es) == 0 {
return http.StatusOK
}
status := es[0].status
for _, e := range es {
if e.status > status {
status = e.status
}
}
return status
}
func (es apiErrors) toResponse() apimodel.ErrorResponse {
resp := apimodel.ErrorResponse{
Errors: make([]apimodel.Error, 0, len(es)),
}
for _, e := range es {
resp.Errors = append(resp.Errors, e.toResponse().Errors...)
}
return resp
}
type errorPathSegment struct {
model string
field string
index int // index may be <0 and this means that it is unset
}
type apiErrorRenderer struct {
errors []error
// formatErrorPath allows to override the default path formatter
// for domain.ValidationError and domain.SliceElementValidationError.
// If formatErrorPath returns an empty slice, an internal server error is rendered.
formatErrorPath func(segments []errorPathSegment) []string
}
var errAPIInternalServerError = apiError{
status: http.StatusInternalServerError,
code: "internal-server-error",
message: "internal server error",
}
func (re apiErrorRenderer) render(w http.ResponseWriter, r *http.Request) {
errs := make(apiErrors, 0, len(re.errors))
for _, err := range re.errors {
var apiErr apiError
var domainErr domain.Error
var paramFormatErr *apimodel.InvalidParamFormatError
switch {
case errors.As(err, &apiErr):
case errors.As(err, &paramFormatErr):
apiErr = re.invalidParamFormatErrorToAPIError(paramFormatErr)
case errors.As(err, &domainErr):
apiErr = re.domainErrorToAPIError(domainErr)
default:
apiErr = errAPIInternalServerError
}
errs = append(errs, apiErr)
}
renderJSON(w, r, errs.status(), errs.toResponse())
}
func (re apiErrorRenderer) invalidParamFormatErrorToAPIError(
paramFormatErr *apimodel.InvalidParamFormatError,
) apiError {
var location string
switch paramFormatErr.Location {
case runtime.ParamLocationPath:
location = "$path"
case runtime.ParamLocationHeader:
location = "$header"
case runtime.ParamLocationQuery:
location = "$query"
case runtime.ParamLocationCookie:
location = "$cookie"
case runtime.ParamLocationUndefined:
fallthrough
default:
location = "$unknown"
}
return apiError{
status: http.StatusBadRequest,
code: "invalid-param-format",
path: []string{location, paramFormatErr.ParamName},
message: paramFormatErr.Err.Error(),
}
}
func (re apiErrorRenderer) domainErrorToAPIError(domainErr domain.Error) apiError {
message := domainErr.Error()
var pathSegments []errorPathSegment
var err error = domainErr
for err != nil {
var validationErr domain.ValidationError
var sliceElementValidationErr domain.SliceElementValidationError
switch {
case errors.As(err, &validationErr):
pathSegments = append(pathSegments, errorPathSegment{
model: validationErr.Model,
field: validationErr.Field,
index: -1,
})
err = validationErr.Unwrap()
message = err.Error()
case errors.As(err, &sliceElementValidationErr):
pathSegments = append(pathSegments, errorPathSegment{
model: sliceElementValidationErr.Model,
field: sliceElementValidationErr.Field,
index: sliceElementValidationErr.Index,
})
err = sliceElementValidationErr.Unwrap()
message = err.Error()
default:
err = nil
}
}
var path []string
if len(pathSegments) > 0 {
if re.formatErrorPath == nil {
return errAPIInternalServerError
}
path = re.formatErrorPath(pathSegments)
if len(path) == 0 {
return errAPIInternalServerError
}
}
var params map[string]any
if withParams, ok := domainErr.(domain.ErrorWithParams); ok {
params = withParams.Params()
}
cloned := make(map[string]any, len(params))
for k, v := range params {
cloned[strcase.ToCamel(k)] = v
}
return apiError{
status: errorTypeToStatusCode(domainErr.Type()),
code: domainErr.Code(),
path: path,
params: cloned,
message: message,
}
}
func errorTypeToStatusCode(code domain.ErrorType) int {
switch code {
case domain.ErrorTypeIncorrectInput:
return http.StatusBadRequest
case domain.ErrorTypeNotFound:
return http.StatusNotFound
case domain.ErrorTypeUnknown:
fallthrough
default:
return http.StatusInternalServerError
}
}