216 lines
4.8 KiB
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, ¶mFormatErr):
|
|
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
|
|
}
|
|
}
|