- config: env-based loading with startup validation, 13 tests - logger: structured slog wrapper with redaction, 10 tests - errors: unified AppError model with HTTP mapping, 18 tests - 41 tests total, build + vet clean
160 lines
5.1 KiB
Go
160 lines
5.1 KiB
Go
// 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")
|
|
}
|