2024-01-31 07:14:18 +00:00
|
|
|
package port
|
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
|
|
|
"net/http"
|
|
|
|
|
2024-04-06 04:32:29 +00:00
|
|
|
"gitea.dwysokinski.me/twhelp/core/internal/domain"
|
|
|
|
"gitea.dwysokinski.me/twhelp/core/internal/port/internal/apimodel"
|
2024-01-31 07:14:18 +00:00
|
|
|
"github.com/ettle/strcase"
|
2024-02-10 11:15:23 +00:00
|
|
|
"github.com/oapi-codegen/runtime"
|
2024-01-31 07:14:18 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
type apiError struct {
|
|
|
|
status int
|
2024-03-18 07:01:47 +00:00
|
|
|
code apimodel.ErrorCode
|
2024-01-31 07:14:18 +00:00
|
|
|
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,
|
2024-02-01 06:48:03 +00:00
|
|
|
Code: e.code,
|
2024-01-31 07:14:18 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2024-03-01 06:46:42 +00:00
|
|
|
type errorPathFormatter func(segments []domain.ErrorPathSegment) []string
|
2024-02-23 06:51:37 +00:00
|
|
|
|
2024-01-31 07:14:18 +00:00
|
|
|
type apiErrorRenderer struct {
|
2024-02-23 06:51:37 +00:00
|
|
|
// errorPathFormatter allows to override the default path formatter
|
2024-01-31 07:14:18 +00:00
|
|
|
// for domain.ValidationError and domain.SliceElementValidationError.
|
2024-02-23 06:51:37 +00:00
|
|
|
// If errorPathFormatter returns an empty slice, an internal server error is rendered.
|
|
|
|
errorPathFormatter errorPathFormatter
|
|
|
|
}
|
|
|
|
|
|
|
|
func (re apiErrorRenderer) withErrorPathFormatter(formatter errorPathFormatter) apiErrorRenderer {
|
|
|
|
// this assignment is done on purpose
|
|
|
|
//nolint:revive
|
|
|
|
re.errorPathFormatter = formatter
|
|
|
|
return re
|
2024-01-31 07:14:18 +00:00
|
|
|
}
|
|
|
|
|
2024-02-10 11:15:23 +00:00
|
|
|
var errAPIInternalServerError = apiError{
|
2024-01-31 07:14:18 +00:00
|
|
|
status: http.StatusInternalServerError,
|
2024-03-18 07:01:47 +00:00
|
|
|
code: apimodel.ErrorCodeInternalServerError,
|
2024-01-31 07:14:18 +00:00
|
|
|
message: "internal server error",
|
|
|
|
}
|
|
|
|
|
2024-02-23 06:51:37 +00:00
|
|
|
func (re apiErrorRenderer) render(w http.ResponseWriter, r *http.Request, errs ...error) {
|
|
|
|
apiErrs := make(apiErrors, 0, len(errs))
|
|
|
|
|
|
|
|
for _, err := range errs {
|
|
|
|
if err == nil {
|
|
|
|
continue
|
|
|
|
}
|
2024-01-31 07:14:18 +00:00
|
|
|
|
|
|
|
var apiErr apiError
|
|
|
|
var domainErr domain.Error
|
|
|
|
var paramFormatErr *apimodel.InvalidParamFormatError
|
|
|
|
|
|
|
|
switch {
|
|
|
|
case errors.As(err, &apiErr):
|
|
|
|
case errors.As(err, ¶mFormatErr):
|
2024-02-10 11:15:23 +00:00
|
|
|
apiErr = re.invalidParamFormatErrorToAPIError(paramFormatErr)
|
2024-01-31 07:14:18 +00:00
|
|
|
case errors.As(err, &domainErr):
|
|
|
|
apiErr = re.domainErrorToAPIError(domainErr)
|
|
|
|
default:
|
2024-02-10 11:15:23 +00:00
|
|
|
apiErr = errAPIInternalServerError
|
2024-01-31 07:14:18 +00:00
|
|
|
}
|
|
|
|
|
2024-02-23 06:51:37 +00:00
|
|
|
apiErrs = append(apiErrs, apiErr)
|
2024-01-31 07:14:18 +00:00
|
|
|
}
|
|
|
|
|
2024-02-23 06:51:37 +00:00
|
|
|
renderJSON(w, r, apiErrs.status(), apiErrs.toResponse())
|
2024-01-31 07:14:18 +00:00
|
|
|
}
|
|
|
|
|
2024-02-10 11:15:23 +00:00
|
|
|
func (re apiErrorRenderer) invalidParamFormatErrorToAPIError(
|
|
|
|
paramFormatErr *apimodel.InvalidParamFormatError,
|
|
|
|
) apiError {
|
2024-02-23 06:51:37 +00:00
|
|
|
location := "$unknown"
|
2024-02-10 11:15:23 +00:00
|
|
|
|
|
|
|
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:
|
2024-02-23 06:51:37 +00:00
|
|
|
// do nothing
|
2024-02-10 11:15:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return apiError{
|
|
|
|
status: http.StatusBadRequest,
|
2024-03-18 07:01:47 +00:00
|
|
|
code: apimodel.ErrorCodeInvalidParamFormat,
|
2024-02-10 11:15:23 +00:00
|
|
|
path: []string{location, paramFormatErr.ParamName},
|
|
|
|
message: paramFormatErr.Err.Error(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-31 07:14:18 +00:00
|
|
|
func (re apiErrorRenderer) domainErrorToAPIError(domainErr domain.Error) apiError {
|
|
|
|
message := domainErr.Error()
|
2024-03-01 06:46:42 +00:00
|
|
|
var pathSegments []domain.ErrorPathSegment
|
2024-01-31 07:14:18 +00:00
|
|
|
|
|
|
|
var err error = domainErr
|
2024-03-01 06:46:42 +00:00
|
|
|
for {
|
|
|
|
var withPath domain.ErrorWithPath
|
2024-01-31 07:14:18 +00:00
|
|
|
|
2024-03-01 06:46:42 +00:00
|
|
|
if !errors.As(err, &withPath) {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
pathSegments = append(pathSegments, withPath.Path())
|
|
|
|
|
2024-03-01 06:59:56 +00:00
|
|
|
u, ok := withPath.(interface {
|
2024-03-01 06:46:42 +00:00
|
|
|
Unwrap() error
|
|
|
|
})
|
|
|
|
if ok {
|
|
|
|
err = u.Unwrap()
|
2024-01-31 07:14:18 +00:00
|
|
|
message = err.Error()
|
2024-03-01 06:46:42 +00:00
|
|
|
} else {
|
2024-01-31 07:14:18 +00:00
|
|
|
err = nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var path []string
|
|
|
|
|
2024-02-10 12:28:41 +00:00
|
|
|
if len(pathSegments) > 0 {
|
2024-02-23 06:51:37 +00:00
|
|
|
if re.errorPathFormatter == nil {
|
2024-02-10 11:15:23 +00:00
|
|
|
return errAPIInternalServerError
|
2024-01-31 07:14:18 +00:00
|
|
|
}
|
|
|
|
|
2024-02-23 06:51:37 +00:00
|
|
|
path = re.errorPathFormatter(pathSegments)
|
2024-01-31 07:14:18 +00:00
|
|
|
|
|
|
|
if len(path) == 0 {
|
2024-02-10 11:15:23 +00:00
|
|
|
return errAPIInternalServerError
|
2024-01-31 07:14:18 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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{
|
2024-02-23 06:51:37 +00:00
|
|
|
status: re.domainErrorTypeToStatusCode(domainErr.Type()),
|
2024-03-18 07:01:47 +00:00
|
|
|
code: apimodel.ErrorCode(domainErr.Code()),
|
2024-01-31 07:14:18 +00:00
|
|
|
path: path,
|
|
|
|
params: cloned,
|
|
|
|
message: message,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-02-23 06:51:37 +00:00
|
|
|
func (re apiErrorRenderer) domainErrorTypeToStatusCode(code domain.ErrorType) int {
|
2024-01-31 07:14:18 +00:00
|
|
|
switch code {
|
2024-02-01 06:48:03 +00:00
|
|
|
case domain.ErrorTypeIncorrectInput:
|
2024-01-31 07:14:18 +00:00
|
|
|
return http.StatusBadRequest
|
2024-02-01 06:48:03 +00:00
|
|
|
case domain.ErrorTypeNotFound:
|
2024-01-31 07:14:18 +00:00
|
|
|
return http.StatusNotFound
|
2024-02-01 06:48:03 +00:00
|
|
|
case domain.ErrorTypeUnknown:
|
2024-01-31 07:14:18 +00:00
|
|
|
fallthrough
|
|
|
|
default:
|
|
|
|
return http.StatusInternalServerError
|
|
|
|
}
|
|
|
|
}
|