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 errorPathFormatter func(segments []errorPathSegment) []string type apiErrorRenderer struct { // errorPathFormatter allows to override the default path formatter // for domain.ValidationError and domain.SliceElementValidationError. // 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 } 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 ...error) { apiErrs := make(apiErrors, 0, len(errs)) for _, err := range errs { if err == nil { continue } 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 } apiErrs = append(apiErrs, apiErr) } renderJSON(w, r, apiErrs.status(), apiErrs.toResponse()) } func (re apiErrorRenderer) invalidParamFormatErrorToAPIError( paramFormatErr *apimodel.InvalidParamFormatError, ) apiError { location := "$unknown" 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: // do nothing } 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.errorPathFormatter == nil { return errAPIInternalServerError } path = re.errorPathFormatter(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: re.domainErrorTypeToStatusCode(domainErr.Type()), code: domainErr.Code(), path: path, params: cloned, message: message, } } func (re apiErrorRenderer) domainErrorTypeToStatusCode(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 } }