123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322 |
- 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."
- }
- }
|