package models import ( "errors" "fmt" "net/http" "github.com/gin-gonic/gin" "github.com/go-playground/validator/v10" ) // CustomError defines the interface for our application's standard errors. type CustomError interface { error // StatusCode returns the HTTP status code appropriate for this error. StatusCode() int // UserMessage returns a user-friendly message for this error. UserMessage() string // InternalCode returns a more specific internal error code or message. InternalCode() string // Unwrap returns the underlying error, if any. Unwrap() error } // baseError is a base implementation of CustomError. type baseError struct { statusCode int userMessage string internalCode string cause error } func (e *baseError) Error() string { if e.cause != nil { return fmt.Sprintf("%s (internal: %s): %v", e.userMessage, e.internalCode, e.cause) } return fmt.Sprintf("%s (internal: %s)", e.userMessage, e.internalCode) } func (e *baseError) StatusCode() int { return e.statusCode } func (e *baseError) UserMessage() string { return e.userMessage } func (e *baseError) InternalCode() string { return e.internalCode } func (e *baseError) Unwrap() error { return e.cause } // --- Specific Error Types --- // ErrNotFound indicates that a requested resource was not found. type ErrNotFound struct { baseError } func NewErrNotFound(internalCode string, cause error) *ErrNotFound { return &ErrNotFound{ baseError{ statusCode: http.StatusNotFound, userMessage: "The requested resource was not found.", internalCode: internalCode, cause: cause, }, } } // ErrValidation indicates that input data failed validation. type ErrValidation struct { baseError // ValidationErrors can hold more specific details about which fields failed. ValidationErrors map[string]string } func NewErrValidation(internalCode string, validationErrors map[string]string, cause error) *ErrValidation { return &ErrValidation{ baseError: baseError{ statusCode: http.StatusBadRequest, userMessage: "Input validation failed. Please check your data.", internalCode: internalCode, cause: cause, }, ValidationErrors: validationErrors, } } // ErrUnauthorized indicates that the request lacks valid authentication credentials. type ErrUnauthorized struct { baseError } func NewErrUnauthorized(internalCode string, cause error) *ErrUnauthorized { return &ErrUnauthorized{ baseError{ statusCode: http.StatusUnauthorized, userMessage: "Authentication is required and has failed or has not yet been provided.", internalCode: internalCode, cause: cause, }, } } // ErrForbidden indicates that the server understood the request but refuses to authorize it. type ErrForbidden struct { baseError } func NewErrForbidden(internalCode string, cause error) *ErrForbidden { return &ErrForbidden{ baseError{ statusCode: http.StatusForbidden, userMessage: "You do not have permission to access this resource.", internalCode: internalCode, cause: cause, }, } } // ErrConflict indicates that the request could not be completed due to a conflict with the current state of the resource. type ErrConflict struct { baseError } func NewErrConflict(internalCode string, cause error) *ErrConflict { return &ErrConflict{ baseError{ statusCode: http.StatusConflict, userMessage: "A conflict occurred with the current state of the resource.", internalCode: internalCode, cause: cause, }, } } // ErrInternalServer indicates an unexpected condition was encountered on the server. type ErrInternalServer struct { baseError } func NewErrInternalServer(internalCode string, cause error) *ErrInternalServer { return &ErrInternalServer{ baseError{ statusCode: http.StatusInternalServerError, userMessage: "An unexpected error occurred on the server. Please try again later.", internalCode: internalCode, cause: cause, }, } } // ErrBadRequest indicates that the server cannot or will not process the request due to something that is perceived to be a client error. type ErrBadRequest struct { baseError } func NewErrBadRequest(internalCode string, cause error) *ErrBadRequest { return &ErrBadRequest{ baseError{ statusCode: http.StatusBadRequest, userMessage: "The request was malformed or invalid.", internalCode: internalCode, cause: cause, }, } } // --- Error Predicates --- // IsErrNotFound checks if an error (or its cause) is an ErrNotFound. func IsErrNotFound(err error) bool { var e *ErrNotFound return errors.As(err, &e) } // IsErrValidation checks if an error (or its cause) is an ErrValidation. func IsErrValidation(err error) bool { var e *ErrValidation return errors.As(err, &e) } // IsErrUnauthorized checks if an error (or its cause) is an ErrUnauthorized. func IsErrUnauthorized(err error) bool { var e *ErrUnauthorized return errors.As(err, &e) } // IsErrForbidden checks if an error (or its cause) is an ErrForbidden. func IsErrForbidden(err error) bool { var e *ErrForbidden return errors.As(err, &e) } // IsErrConflict checks if an error (or its cause) is an ErrConflict. func IsErrConflict(err error) bool { var e *ErrConflict return errors.As(err, &e) } // IsErrInternalServer checks if an error (or its cause) is an ErrInternalServer. func IsErrInternalServer(err error) bool { var e *ErrInternalServer return errors.As(err, &e) } // IsErrForeignKeyViolation is a placeholder for checking foreign key errors. // This would typically be database-specific. // For SQLite, you might check for strings like "FOREIGN KEY constraint failed". // For Postgres, it would be a specific error code. func IsErrForeignKeyViolation(err error) bool { if err == nil { return false } // This is a simplistic check. In a real app, you'd use driver-specific error codes/types. // e.g., for github.com/mattn/go-sqlite3: // if sqliteErr, ok := err.(sqlite3.Error); ok { // return sqliteErr.Code == sqlite3.ErrConstraintForeignKey // } return false // Placeholder, needs actual DB driver error checking } // --- Helper for handlers --- // RespondWithError checks if the error is a CustomError and sends an appropriate JSON response. // Otherwise, it sends a generic 500 error. func RespondWithError(c GinContext, err error) { var customErr CustomError if asCustomErr, ok := err.(CustomError); ok { // Check if it directly implements CustomError customErr = asCustomErr } else if unwrapErr := AsCustomError(err); unwrapErr != nil { // Check if it wraps a CustomError customErr = unwrapErr } if customErr != nil { response := gin.H{ "status": "error", "message": customErr.UserMessage(), "code": customErr.InternalCode(), } // Add validation details for validation errors if valErr, ok := customErr.(*ErrValidation); ok && valErr.ValidationErrors != nil { response["details"] = valErr.ValidationErrors } c.JSON(customErr.StatusCode(), response) return } // Fallback for non-custom errors (log them as they are unexpected) // In a real app, you'd log this error with more details. // log.Printf("Unhandled error: %v", err) // Example logging c.JSON(http.StatusInternalServerError, gin.H{ "status": "error", "message": "An unexpected internal server error occurred.", "code": "INTERNAL_SERVER_ERROR", }) } // GinContext is an interface to abstract gin.Context for easier testing or alternative router usage. type GinContext interface { JSON(code int, obj interface{}) // Add other gin.Context methods you use in RespondWithError if any } // AsCustomError attempts to unwrap err until a CustomError is found or err is nil. func AsCustomError(err error) CustomError { for err != nil { if ce, ok := err.(CustomError); ok { return ce } err = Unwrap(err) } return nil } // Unwrap is a helper to call Unwrap on an error if the method exists. func Unwrap(err error) error { u, ok := err.(interface{ Unwrap() error }) if !ok { return nil } return u.Unwrap() } // Helper type for gin.H to avoid direct dependency in models if preferred, though gin.H is just map[string]interface{} type ginH map[string]interface{} // ExtractValidationErrors converts validator.ValidationErrors to a map[string]string. func ExtractValidationErrors(err error) map[string]string { var ve validator.ValidationErrors if errors.As(err, &ve) { out := make(map[string]string) for _, fe := range ve { out[fe.Field()] = msgForTag(fe.Tag(), fe.Param()) } return out } return nil } // msgForTag returns a user-friendly message for a given validation tag. func msgForTag(tag string, param string) string { switch tag { case "required": return "This field is required." case "min": return fmt.Sprintf("This field must be at least %s characters long.", param) case "max": return fmt.Sprintf("This field must be at most %s characters long.", param) case "email": return "Invalid email format." case "oneof": return fmt.Sprintf("This field must be one of: %s.", param) // Add more cases for other tags as needed default: return "Invalid value." } }