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 pathElems []errorPathSegment var err error = domainErr for err != nil { var validationErr domain.ValidationError var sliceElementValidationErr domain.SliceElementValidationError switch { case errors.As(err, &validationErr): pathElems = append(pathElems, errorPathSegment{ model: validationErr.Model, field: validationErr.Field, index: -1, }) err = validationErr.Unwrap() message = err.Error() case errors.As(err, &sliceElementValidationErr): pathElems = append(pathElems, errorPathSegment{ model: sliceElementValidationErr.Model, field: sliceElementValidationErr.Field, index: sliceElementValidationErr.Index, }) err = sliceElementValidationErr.Unwrap() message = err.Error() default: err = nil } } var path []string if len(pathElems) > 0 { if re.formatErrorPath == nil { return errAPIInternalServerError } path = re.formatErrorPath(pathElems) 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 } }