// Package errors provides a unified error model for the Cyrene platform. // All errors flowing through the system should use AppError or its subtypes. // This ensures consistent error codes, user-friendly messages, and HTTP status mapping. package errors import ( "fmt" "net/http" ) // AppError is the unified application error type. type AppError struct { Code string // Machine-readable error code (e.g., "USER_NOT_FOUND") Message string // Human-readable message (safe to show to users) Cause error // Original error (internal only, not exposed to clients) HTTPCode int // HTTP status code } // Error implements the error interface. func (e *AppError) Error() string { if e.Cause != nil { return fmt.Sprintf("[%s] %s: %v", e.Code, e.Message, e.Cause) } return fmt.Sprintf("[%s] %s", e.Code, e.Message) } // Unwrap returns the underlying cause error. func (e *AppError) Unwrap() error { return e.Cause } // --- Pre-defined common errors --- // NewNotFound creates a NOT_FOUND error. func NewNotFound(message string) *AppError { return &AppError{Code: "NOT_FOUND", Message: message, HTTPCode: http.StatusNotFound} } // NewUnauthorized creates an UNAUTHORIZED error. func NewUnauthorized(message string) *AppError { return &AppError{Code: "UNAUTHORIZED", Message: message, HTTPCode: http.StatusUnauthorized} } // NewForbidden creates a FORBIDDEN error. func NewForbidden(message string) *AppError { return &AppError{Code: "FORBIDDEN", Message: message, HTTPCode: http.StatusForbidden} } // NewBadRequest creates a BAD_REQUEST error. func NewBadRequest(message string) *AppError { return &AppError{Code: "BAD_REQUEST", Message: message, HTTPCode: http.StatusBadRequest} } // NewConflict creates a CONFLICT error. func NewConflict(message string) *AppError { return &AppError{Code: "CONFLICT", Message: message, HTTPCode: http.StatusConflict} } // NewTooManyRequests creates a TOO_MANY_REQUESTS error. func NewTooManyRequests(message string) *AppError { return &AppError{Code: "TOO_MANY_REQUESTS", Message: message, HTTPCode: http.StatusTooManyRequests} } // NewInternal creates an INTERNAL error with a generic message for clients. // The cause is logged internally but not exposed. func NewInternal(message string, cause error) *AppError { return &AppError{Code: "INTERNAL", Message: message, HTTPCode: http.StatusInternalServerError, Cause: cause} } // NewValidation creates a VALIDATION error. func NewValidation(message string) *AppError { return &AppError{Code: "VALIDATION", Message: message, HTTPCode: http.StatusBadRequest} } // --- Constructors with cause --- // Wrap wraps an existing error with an application error. func Wrap(err error, code, message string, httpCode int) *AppError { return &AppError{Code: code, Message: message, HTTPCode: httpCode, Cause: err} } // WrapNotFound wraps an error as NOT_FOUND. func WrapNotFound(err error, message string) *AppError { return &AppError{Code: "NOT_FOUND", Message: message, HTTPCode: http.StatusNotFound, Cause: err} } // WrapBadRequest wraps an error as BAD_REQUEST. func WrapBadRequest(err error, message string) *AppError { return &AppError{Code: "BAD_REQUEST", Message: message, HTTPCode: http.StatusBadRequest, Cause: err} } // WrapInternal wraps an error as INTERNAL. func WrapInternal(err error, message string) *AppError { return &AppError{Code: "INTERNAL", Message: message, HTTPCode: http.StatusInternalServerError, Cause: err} } // --- Helpers --- // IsAppError checks if the error is an AppError. func IsAppError(err error) (*AppError, bool) { if err == nil { return nil, false } ae, ok := err.(*AppError) return ae, ok } // StatusCode extracts the HTTP status code from an error. // Returns 500 if the error is not an AppError. func StatusCode(err error) int { if ae, ok := IsAppError(err); ok { return ae.HTTPCode } return http.StatusInternalServerError } // Message extracts the user-safe message from an error. // Returns a generic message if the error is not an AppError. func Message(err error) string { if ae, ok := IsAppError(err); ok { return ae.Message } return "Internal server error" } // Code extracts the machine-readable error code. // Returns "INTERNAL" if the error is not an AppError. func Code(err error) string { if ae, ok := IsAppError(err); ok { return ae.Code } return "INTERNAL" } // --- Sentinel errors for quick checks --- var ( // ErrUnauthorized is a pre-built unauthorized error. ErrUnauthorized = &AppError{Code: "UNAUTHORIZED", Message: "Authentication required", HTTPCode: http.StatusUnauthorized} // ErrForbidden is a pre-built forbidden error. ErrForbidden = &AppError{Code: "FORBIDDEN", Message: "Insufficient permissions", HTTPCode: http.StatusForbidden} // ErrInternal is a pre-built internal error. ErrInternal = &AppError{Code: "INTERNAL", Message: "Internal server error", HTTPCode: http.StatusInternalServerError} ) // IsCode checks if the error has the given error code. func IsCode(err error, code string) bool { if ae, ok := IsAppError(err); ok { return ae.Code == code } return false } // IsNotFound checks if the error is a NOT_FOUND error. func IsNotFound(err error) bool { return IsCode(err, "NOT_FOUND") }