first commit
This commit is contained in:
@@ -0,0 +1,329 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc/codes"
|
||||
)
|
||||
|
||||
// Builder provides fluent API for building errors
|
||||
type Builder struct {
|
||||
code string
|
||||
message string
|
||||
category string
|
||||
httpStatus int
|
||||
grpcCode codes.Code
|
||||
metadata Metadata
|
||||
cause error
|
||||
}
|
||||
|
||||
// NewBuilder creates a new error builder
|
||||
func NewBuilder() *Builder {
|
||||
return &Builder{
|
||||
metadata: make(Metadata),
|
||||
}
|
||||
}
|
||||
|
||||
// Code sets error code
|
||||
func (b *Builder) Code(code string) *Builder {
|
||||
b.code = code
|
||||
info := GetErrorInfo(code)
|
||||
b.category = info.Category
|
||||
b.httpStatus = info.HTTPStatus
|
||||
b.grpcCode = info.GRPCCode
|
||||
return b
|
||||
}
|
||||
|
||||
// Message sets error message
|
||||
func (b *Builder) Message(message string) *Builder {
|
||||
b.message = message
|
||||
return b
|
||||
}
|
||||
|
||||
// Category sets error category
|
||||
func (b *Builder) Category(category string) *Builder {
|
||||
b.category = category
|
||||
return b
|
||||
}
|
||||
|
||||
// HTTPStatus sets HTTP status code
|
||||
func (b *Builder) HTTPStatus(status int) *Builder {
|
||||
b.httpStatus = status
|
||||
return b
|
||||
}
|
||||
|
||||
// GRPCCode sets gRPC code
|
||||
func (b *Builder) GRPCCode(code codes.Code) *Builder {
|
||||
b.grpcCode = code
|
||||
return b
|
||||
}
|
||||
|
||||
// Metadata adds metadata
|
||||
func (b *Builder) Metadata(key string, value interface{}) *Builder {
|
||||
if b.metadata == nil {
|
||||
b.metadata = make(Metadata)
|
||||
}
|
||||
b.metadata[key] = value
|
||||
return b
|
||||
}
|
||||
|
||||
// Metadatas adds multiple metadata
|
||||
func (b *Builder) Metadatas(metadata Metadata) *Builder {
|
||||
if b.metadata == nil {
|
||||
b.metadata = make(Metadata)
|
||||
}
|
||||
for k, v := range metadata {
|
||||
b.metadata[k] = v
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// Cause sets underlying error
|
||||
func (b *Builder) Cause(err error) *Builder {
|
||||
b.cause = err
|
||||
return b
|
||||
}
|
||||
|
||||
// Timestamp adds timestamp
|
||||
func (b *Builder) Timestamp(t time.Time) *Builder {
|
||||
b.Metadata("timestamp", t.Format(time.RFC3339))
|
||||
return b
|
||||
}
|
||||
|
||||
// RequestID adds request ID
|
||||
func (b *Builder) RequestID(id string) *Builder {
|
||||
b.Metadata("request_id", id)
|
||||
return b
|
||||
}
|
||||
|
||||
// UserID adds user ID
|
||||
func (b *Builder) UserID(id string) *Builder {
|
||||
b.Metadata("user_id", id)
|
||||
return b
|
||||
}
|
||||
|
||||
// Service adds service name
|
||||
func (b *Builder) Service(name string) *Builder {
|
||||
b.Metadata("service", name)
|
||||
return b
|
||||
}
|
||||
|
||||
// Operation adds operation name
|
||||
func (b *Builder) Operation(name string) *Builder {
|
||||
b.Metadata("operation", name)
|
||||
return b
|
||||
}
|
||||
|
||||
// Retryable sets retryable flag
|
||||
func (b *Builder) Retryable(retryable bool) *Builder {
|
||||
b.Metadata("retryable", retryable)
|
||||
return b
|
||||
}
|
||||
|
||||
// Severity sets error severity
|
||||
func (b *Builder) Severity(severity string) *Builder {
|
||||
b.Metadata("severity", severity)
|
||||
return b
|
||||
}
|
||||
|
||||
// Component adds component name
|
||||
func (b *Builder) Component(name string) *Builder {
|
||||
b.Metadata("component", name)
|
||||
return b
|
||||
}
|
||||
|
||||
// Build creates the final error
|
||||
func (b *Builder) Build() Error {
|
||||
// Use defaults if not set
|
||||
if b.code == "" {
|
||||
b.code = ErrCodeInternalError
|
||||
}
|
||||
if b.message == "" {
|
||||
b.message = "An error occurred"
|
||||
}
|
||||
if b.category == "" {
|
||||
b.category = CategoryInternal
|
||||
}
|
||||
if b.httpStatus == 0 {
|
||||
b.httpStatus = http.StatusInternalServerError
|
||||
}
|
||||
if b.grpcCode == codes.OK {
|
||||
b.grpcCode = codes.Internal
|
||||
}
|
||||
if b.metadata == nil {
|
||||
b.metadata = make(Metadata)
|
||||
}
|
||||
if _, exists := b.metadata["timestamp"]; !exists {
|
||||
b.Metadata("timestamp", time.Now().Format(time.RFC3339))
|
||||
}
|
||||
|
||||
return &AppError{
|
||||
code: b.code,
|
||||
message: b.message,
|
||||
category: b.category,
|
||||
httpStatus: b.httpStatus,
|
||||
grpcCode: b.grpcCode,
|
||||
metadata: b.metadata,
|
||||
cause: b.cause,
|
||||
timestamp: time.Now(),
|
||||
stackTrace: captureStackTrace(),
|
||||
}
|
||||
}
|
||||
|
||||
// NewValidationError creates validation error builder
|
||||
func NewValidationError() *Builder {
|
||||
return NewBuilder().
|
||||
Code(ErrCodeValidationFailed).
|
||||
Category(CategoryValidation).
|
||||
HTTPStatus(http.StatusBadRequest).
|
||||
GRPCCode(codes.InvalidArgument).
|
||||
Severity("warning")
|
||||
}
|
||||
|
||||
// NotFoundError creates not found error builder
|
||||
func NotFoundError() *Builder {
|
||||
return NewBuilder().
|
||||
Code(ErrCodeNotFound).
|
||||
Category(CategoryNotFound).
|
||||
HTTPStatus(http.StatusNotFound).
|
||||
GRPCCode(codes.NotFound).
|
||||
Severity("info")
|
||||
}
|
||||
|
||||
// UnauthorizedError creates unauthorized error builder
|
||||
func UnauthorizedError() *Builder {
|
||||
return NewBuilder().
|
||||
Code(ErrCodeUnauthorized).
|
||||
Category(CategoryUnauthorized).
|
||||
HTTPStatus(http.StatusUnauthorized).
|
||||
GRPCCode(codes.Unauthenticated).
|
||||
Severity("warning")
|
||||
}
|
||||
|
||||
// ForbiddenError creates forbidden error builder
|
||||
func ForbiddenError() *Builder {
|
||||
return NewBuilder().
|
||||
Code(ErrCodeForbidden).
|
||||
Category(CategoryForbidden).
|
||||
HTTPStatus(http.StatusForbidden).
|
||||
GRPCCode(codes.PermissionDenied).
|
||||
Severity("warning")
|
||||
}
|
||||
|
||||
// AlreadyExistsError creates already exists error builder
|
||||
func AlreadyExistsError() *Builder {
|
||||
return NewBuilder().
|
||||
Code(ErrCodeConflict).
|
||||
Category(CategoryConflict).
|
||||
HTTPStatus(http.StatusConflict).
|
||||
GRPCCode(codes.AlreadyExists).
|
||||
Severity("warning")
|
||||
}
|
||||
|
||||
// DuplicateError creates duplicate entry error builder
|
||||
func DuplicateError() *Builder {
|
||||
return NewBuilder().
|
||||
Code(ErrCodeDuplicateEntry).
|
||||
Category(CategoryConflict).
|
||||
HTTPStatus(http.StatusConflict).
|
||||
GRPCCode(codes.AlreadyExists).
|
||||
Severity("warning")
|
||||
}
|
||||
|
||||
// ConflictError creates conflict error builder
|
||||
func ConflictError() *Builder {
|
||||
return NewBuilder().
|
||||
Code(ErrCodeConflict).
|
||||
Category(CategoryConflict).
|
||||
HTTPStatus(http.StatusConflict).
|
||||
GRPCCode(codes.AlreadyExists).
|
||||
Severity("warning")
|
||||
}
|
||||
|
||||
// InternalError creates internal error builder
|
||||
func InternalError() *Builder {
|
||||
return NewBuilder().
|
||||
Code(ErrCodeInternalError).
|
||||
Category(CategoryInternal).
|
||||
HTTPStatus(http.StatusInternalServerError).
|
||||
GRPCCode(codes.Internal).
|
||||
Severity("error")
|
||||
}
|
||||
|
||||
// ExternalError creates external error builder
|
||||
func ExternalError() *Builder {
|
||||
return NewBuilder().
|
||||
Code(ErrCodeExternalError).
|
||||
Category(CategoryExternal).
|
||||
HTTPStatus(http.StatusBadGateway).
|
||||
GRPCCode(codes.Unavailable).
|
||||
Severity("error").
|
||||
Retryable(true)
|
||||
}
|
||||
|
||||
// TimeoutError creates timeout error builder
|
||||
func TimeoutError() *Builder {
|
||||
return NewBuilder().
|
||||
Code(ErrCodeTimeout).
|
||||
Category(CategoryTimeout).
|
||||
HTTPStatus(http.StatusRequestTimeout).
|
||||
GRPCCode(codes.DeadlineExceeded).
|
||||
Severity("warning").
|
||||
Retryable(true)
|
||||
}
|
||||
|
||||
// BusinessError creates business error builder
|
||||
func BusinessError() *Builder {
|
||||
return NewBuilder().
|
||||
Code(ErrCodeBusinessRule).
|
||||
Category(CategoryBusiness).
|
||||
HTTPStatus(http.StatusBadRequest).
|
||||
GRPCCode(codes.FailedPrecondition).
|
||||
Severity("warning")
|
||||
}
|
||||
|
||||
// DatabaseError creates database error builder
|
||||
func DatabaseError() *Builder {
|
||||
return NewBuilder().
|
||||
Code(ErrCodeDatabaseError).
|
||||
Category(CategoryDatabase).
|
||||
HTTPStatus(http.StatusInternalServerError).
|
||||
GRPCCode(codes.Internal).
|
||||
Severity("error")
|
||||
}
|
||||
|
||||
// NetworkError creates network error builder
|
||||
func NetworkError() *Builder {
|
||||
return NewBuilder().
|
||||
Code(ErrCodeNetworkError).
|
||||
Category(CategoryNetwork).
|
||||
HTTPStatus(http.StatusBadGateway).
|
||||
GRPCCode(codes.Unavailable).
|
||||
Severity("error").
|
||||
Retryable(true)
|
||||
}
|
||||
|
||||
// CustomError creates custom error builder
|
||||
func CustomError(code, message string) *Builder {
|
||||
return NewBuilder().
|
||||
Code(code).
|
||||
Message(message)
|
||||
}
|
||||
|
||||
// FromError creates builder from existing error
|
||||
func FromErrorBuilder(err error) *Builder {
|
||||
appErr := FromError(err)
|
||||
if appErr == nil {
|
||||
return NewBuilder()
|
||||
}
|
||||
|
||||
return NewBuilder().
|
||||
Code(appErr.Code()).
|
||||
Message(appErr.Error()).
|
||||
Category(appErr.Category()).
|
||||
HTTPStatus(appErr.HTTPStatus()).
|
||||
GRPCCode(appErr.GRPCCode()).
|
||||
Metadatas(appErr.Metadata()).
|
||||
Cause(appErr.Cause())
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"google.golang.org/grpc/codes"
|
||||
)
|
||||
|
||||
// Error categories
|
||||
const (
|
||||
CategoryValidation = "validation"
|
||||
CategoryNotFound = "not_found"
|
||||
CategoryUnauthorized = "unauthorized"
|
||||
CategoryForbidden = "forbidden"
|
||||
CategoryConflict = "conflict"
|
||||
CategoryRateLimit = "rate_limit"
|
||||
CategoryInternal = "internal"
|
||||
CategoryExternal = "external"
|
||||
CategoryBusiness = "business"
|
||||
CategoryTimeout = "timeout"
|
||||
CategoryNetwork = "network"
|
||||
CategoryDatabase = "database"
|
||||
)
|
||||
|
||||
// Standard error codes
|
||||
const (
|
||||
// Validation errors
|
||||
ErrCodeValidationFailed = "VALIDATION_FAILED"
|
||||
ErrCodeInvalidInput = "INVALID_INPUT"
|
||||
ErrCodeMissingField = "MISSING_FIELD"
|
||||
ErrCodeInvalidFormat = "INVALID_FORMAT"
|
||||
ErrCodeInvalidLength = "INVALID_LENGTH"
|
||||
ErrCodeInvalidRange = "INVALID_RANGE"
|
||||
|
||||
// Not found errors
|
||||
ErrCodeNotFound = "NOT_FOUND"
|
||||
ErrCodeUserNotFound = "USER_NOT_FOUND"
|
||||
ErrCodeResourceNotFound = "RESOURCE_NOT_FOUND"
|
||||
ErrCodeDataNotFound = "DATA_NOT_FOUND"
|
||||
|
||||
// Authorization errors
|
||||
ErrCodeUnauthorized = "UNAUTHORIZED"
|
||||
ErrCodeInvalidToken = "INVALID_TOKEN"
|
||||
ErrCodeTokenExpired = "TOKEN_EXPIRED"
|
||||
ErrCodeInvalidCredentials = "INVALID_CREDENTIALS"
|
||||
|
||||
// Forbidden errors
|
||||
ErrCodeForbidden = "FORBIDDEN"
|
||||
ErrCodeInsufficientRights = "INSUFFICIENT_RIGHTS"
|
||||
ErrCodeAccessDenied = "ACCESS_DENIED"
|
||||
|
||||
// Conflict errors
|
||||
ErrCodeConflict = "CONFLICT"
|
||||
ErrCodeDuplicateEntry = "DUPLICATE_ENTRY"
|
||||
ErrCodeResourceLocked = "RESOURCE_LOCKED"
|
||||
ErrCodeConcurrentUpdate = "CONCURRENT_UPDATE"
|
||||
|
||||
// Rate limit errors
|
||||
ErrCodeRateLimitExceeded = "RATE_LIMIT_EXCEEDED"
|
||||
ErrCodeTooManyRequests = "TOO_MANY_REQUESTS"
|
||||
ErrCodeQuotaExceeded = "QUOTA_EXCEEDED"
|
||||
|
||||
// Internal errors
|
||||
ErrCodeInternalError = "INTERNAL_ERROR"
|
||||
ErrCodeUnexpectedError = "UNEXPECTED_ERROR"
|
||||
ErrCodeServiceUnavailable = "SERVICE_UNAVAILABLE"
|
||||
ErrCodeConfigurationError = "CONFIGURATION_ERROR"
|
||||
|
||||
// External errors
|
||||
ErrCodeExternalError = "EXTERNAL_ERROR"
|
||||
ErrCodeThirdPartyError = "THIRD_PARTY_ERROR"
|
||||
ErrCodeAPIError = "API_ERROR"
|
||||
|
||||
// Business errors
|
||||
ErrCodeBusinessRule = "BUSINESS_RULE"
|
||||
ErrCodeInsufficientBalance = "INSUFFICIENT_BALANCE"
|
||||
ErrCodeAccountSuspended = "ACCOUNT_SUSPENDED"
|
||||
|
||||
// Timeout errors
|
||||
ErrCodeTimeout = "TIMEOUT"
|
||||
ErrCodeRequestTimeout = "REQUEST_TIMEOUT"
|
||||
ErrCodeConnectionTimeout = "CONNECTION_TIMEOUT"
|
||||
|
||||
// Network errors
|
||||
ErrCodeNetworkError = "NETWORK_ERROR"
|
||||
ErrCodeConnectionFailed = "CONNECTION_FAILED"
|
||||
ErrCodeConnectionLost = "CONNECTION_LOST"
|
||||
|
||||
// Database errors
|
||||
ErrCodeDatabaseError = "DATABASE_ERROR"
|
||||
ErrCodeQueryFailed = "QUERY_FAILED"
|
||||
ErrCodeTransactionFailed = "TRANSACTION_FAILED"
|
||||
)
|
||||
|
||||
// ErrorInfo contains error metadata
|
||||
type ErrorInfo struct {
|
||||
Code string
|
||||
Category string
|
||||
HTTPStatus int
|
||||
GRPCCode codes.Code
|
||||
}
|
||||
|
||||
// ErrorTemplate for custom error types
|
||||
type ErrorTemplate struct {
|
||||
Code string
|
||||
Message string
|
||||
Category string
|
||||
HTTPStatus int
|
||||
GRPCCode codes.Code
|
||||
Retryable bool
|
||||
}
|
||||
|
||||
var (
|
||||
errorCodes = make(map[string]ErrorInfo)
|
||||
errorCodesMu sync.RWMutex
|
||||
errorTemplates = make(map[string]*ErrorTemplate)
|
||||
errorTemplatesMu sync.RWMutex
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Initialize standard error codes
|
||||
initializeErrorCodes()
|
||||
}
|
||||
|
||||
// initializeErrorCodes sets up standard error codes
|
||||
func initializeErrorCodes() {
|
||||
// Validation errors
|
||||
registerErrorCode(ErrCodeValidationFailed, CategoryValidation, http.StatusBadRequest, codes.InvalidArgument)
|
||||
registerErrorCode(ErrCodeInvalidInput, CategoryValidation, http.StatusBadRequest, codes.InvalidArgument)
|
||||
registerErrorCode(ErrCodeMissingField, CategoryValidation, http.StatusBadRequest, codes.InvalidArgument)
|
||||
registerErrorCode(ErrCodeInvalidFormat, CategoryValidation, http.StatusBadRequest, codes.InvalidArgument)
|
||||
registerErrorCode(ErrCodeInvalidLength, CategoryValidation, http.StatusBadRequest, codes.InvalidArgument)
|
||||
registerErrorCode(ErrCodeInvalidRange, CategoryValidation, http.StatusBadRequest, codes.InvalidArgument)
|
||||
|
||||
// Not found errors
|
||||
registerErrorCode(ErrCodeNotFound, CategoryNotFound, http.StatusNotFound, codes.NotFound)
|
||||
registerErrorCode(ErrCodeUserNotFound, CategoryNotFound, http.StatusNotFound, codes.NotFound)
|
||||
registerErrorCode(ErrCodeResourceNotFound, CategoryNotFound, http.StatusNotFound, codes.NotFound)
|
||||
registerErrorCode(ErrCodeDataNotFound, CategoryNotFound, http.StatusNotFound, codes.NotFound)
|
||||
|
||||
// Authorization errors
|
||||
registerErrorCode(ErrCodeUnauthorized, CategoryUnauthorized, http.StatusUnauthorized, codes.Unauthenticated)
|
||||
registerErrorCode(ErrCodeInvalidToken, CategoryUnauthorized, http.StatusUnauthorized, codes.Unauthenticated)
|
||||
registerErrorCode(ErrCodeTokenExpired, CategoryUnauthorized, http.StatusUnauthorized, codes.Unauthenticated)
|
||||
registerErrorCode(ErrCodeInvalidCredentials, CategoryUnauthorized, http.StatusUnauthorized, codes.Unauthenticated)
|
||||
|
||||
// Forbidden errors
|
||||
registerErrorCode(ErrCodeForbidden, CategoryForbidden, http.StatusForbidden, codes.PermissionDenied)
|
||||
registerErrorCode(ErrCodeInsufficientRights, CategoryForbidden, http.StatusForbidden, codes.PermissionDenied)
|
||||
registerErrorCode(ErrCodeAccessDenied, CategoryForbidden, http.StatusForbidden, codes.PermissionDenied)
|
||||
|
||||
// Conflict errors
|
||||
registerErrorCode(ErrCodeConflict, CategoryConflict, http.StatusConflict, codes.AlreadyExists)
|
||||
registerErrorCode(ErrCodeDuplicateEntry, CategoryConflict, http.StatusConflict, codes.AlreadyExists)
|
||||
registerErrorCode(ErrCodeResourceLocked, CategoryConflict, http.StatusLocked, codes.Aborted)
|
||||
registerErrorCode(ErrCodeConcurrentUpdate, CategoryConflict, http.StatusConflict, codes.Aborted)
|
||||
|
||||
// Rate limit errors
|
||||
registerErrorCode(ErrCodeRateLimitExceeded, CategoryRateLimit, http.StatusTooManyRequests, codes.ResourceExhausted)
|
||||
registerErrorCode(ErrCodeTooManyRequests, CategoryRateLimit, http.StatusTooManyRequests, codes.ResourceExhausted)
|
||||
registerErrorCode(ErrCodeQuotaExceeded, CategoryRateLimit, http.StatusTooManyRequests, codes.ResourceExhausted)
|
||||
|
||||
// Internal errors
|
||||
registerErrorCode(ErrCodeInternalError, CategoryInternal, http.StatusInternalServerError, codes.Internal)
|
||||
registerErrorCode(ErrCodeUnexpectedError, CategoryInternal, http.StatusInternalServerError, codes.Internal)
|
||||
registerErrorCode(ErrCodeServiceUnavailable, CategoryInternal, http.StatusServiceUnavailable, codes.Unavailable)
|
||||
registerErrorCode(ErrCodeConfigurationError, CategoryInternal, http.StatusInternalServerError, codes.Internal)
|
||||
|
||||
// External errors
|
||||
registerErrorCode(ErrCodeExternalError, CategoryExternal, http.StatusBadGateway, codes.Unavailable)
|
||||
registerErrorCode(ErrCodeThirdPartyError, CategoryExternal, http.StatusBadGateway, codes.Unavailable)
|
||||
registerErrorCode(ErrCodeAPIError, CategoryExternal, http.StatusBadGateway, codes.Unavailable)
|
||||
|
||||
// Business errors
|
||||
registerErrorCode(ErrCodeBusinessRule, CategoryBusiness, http.StatusBadRequest, codes.FailedPrecondition)
|
||||
registerErrorCode(ErrCodeInsufficientBalance, CategoryBusiness, http.StatusBadRequest, codes.FailedPrecondition)
|
||||
registerErrorCode(ErrCodeAccountSuspended, CategoryBusiness, http.StatusForbidden, codes.PermissionDenied)
|
||||
|
||||
// Timeout errors
|
||||
registerErrorCode(ErrCodeTimeout, CategoryTimeout, http.StatusRequestTimeout, codes.DeadlineExceeded)
|
||||
registerErrorCode(ErrCodeRequestTimeout, CategoryTimeout, http.StatusRequestTimeout, codes.DeadlineExceeded)
|
||||
registerErrorCode(ErrCodeConnectionTimeout, CategoryTimeout, http.StatusRequestTimeout, codes.DeadlineExceeded)
|
||||
|
||||
// Network errors
|
||||
registerErrorCode(ErrCodeNetworkError, CategoryNetwork, http.StatusBadGateway, codes.Unavailable)
|
||||
registerErrorCode(ErrCodeConnectionFailed, CategoryNetwork, http.StatusBadGateway, codes.Unavailable)
|
||||
registerErrorCode(ErrCodeConnectionLost, CategoryNetwork, http.StatusBadGateway, codes.Unavailable)
|
||||
|
||||
// Database errors
|
||||
registerErrorCode(ErrCodeDatabaseError, CategoryDatabase, http.StatusInternalServerError, codes.Internal)
|
||||
registerErrorCode(ErrCodeQueryFailed, CategoryDatabase, http.StatusInternalServerError, codes.Internal)
|
||||
registerErrorCode(ErrCodeTransactionFailed, CategoryDatabase, http.StatusInternalServerError, codes.Aborted)
|
||||
}
|
||||
|
||||
// registerErrorCode registers an error code
|
||||
func registerErrorCode(code, category string, httpStatus int, grpcCode codes.Code) {
|
||||
errorCodesMu.Lock()
|
||||
defer errorCodesMu.Unlock()
|
||||
errorCodes[code] = ErrorInfo{
|
||||
Code: code,
|
||||
Category: category,
|
||||
HTTPStatus: httpStatus,
|
||||
GRPCCode: grpcCode,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterErrorCode registers a custom error code
|
||||
func RegisterErrorCode(code, category string, httpStatus int, grpcCode codes.Code) {
|
||||
registerErrorCode(code, category, httpStatus, grpcCode)
|
||||
}
|
||||
|
||||
// GetErrorInfo returns error information for a code
|
||||
func GetErrorInfo(code string) ErrorInfo {
|
||||
errorCodesMu.RLock()
|
||||
defer errorCodesMu.RUnlock()
|
||||
|
||||
if info, exists := errorCodes[code]; exists {
|
||||
return info
|
||||
}
|
||||
|
||||
return ErrorInfo{
|
||||
Code: code,
|
||||
Category: CategoryInternal,
|
||||
HTTPStatus: http.StatusInternalServerError,
|
||||
GRPCCode: codes.Internal,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterErrorTemplate registers a custom error template
|
||||
func RegisterErrorTemplate(name string, template *ErrorTemplate) {
|
||||
errorTemplatesMu.Lock()
|
||||
defer errorTemplatesMu.Unlock()
|
||||
errorTemplates[name] = template
|
||||
}
|
||||
|
||||
// GetErrorTemplate returns error template by name
|
||||
func GetErrorTemplate(name string) *ErrorTemplate {
|
||||
errorTemplatesMu.RLock()
|
||||
defer errorTemplatesMu.RUnlock()
|
||||
return errorTemplates[name]
|
||||
}
|
||||
|
||||
// GetAllErrorCodes returns all registered error codes
|
||||
func GetAllErrorCodes() map[string]ErrorInfo {
|
||||
errorCodesMu.RLock()
|
||||
defer errorCodesMu.RUnlock()
|
||||
|
||||
result := make(map[string]ErrorInfo)
|
||||
for k, v := range errorCodes {
|
||||
result[k] = v
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// GetErrorCodesByCategory returns error codes by category
|
||||
func GetErrorCodesByCategory(category string) map[string]ErrorInfo {
|
||||
errorCodesMu.RLock()
|
||||
defer errorCodesMu.RUnlock()
|
||||
|
||||
result := make(map[string]ErrorInfo)
|
||||
for k, v := range errorCodes {
|
||||
if v.Category == category {
|
||||
result[k] = v
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// IsRetryable checks if error is retryable
|
||||
func IsRetryable(code string) bool {
|
||||
info := GetErrorInfo(code)
|
||||
switch info.Category {
|
||||
case CategoryNetwork, CategoryTimeout, CategoryExternal:
|
||||
return true
|
||||
case CategoryDatabase:
|
||||
return strings.Contains(code, "TIMEOUT") || strings.Contains(code, "CONNECTION")
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// IsClientError checks if error is client-side (4xx)
|
||||
func IsClientError(code string) bool {
|
||||
info := GetErrorInfo(code)
|
||||
return info.HTTPStatus >= 400 && info.HTTPStatus < 500
|
||||
}
|
||||
|
||||
// IsServerError checks if error is server-side (5xx)
|
||||
func IsServerError(code string) bool {
|
||||
info := GetErrorInfo(code)
|
||||
return info.HTTPStatus >= 500
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc/codes"
|
||||
)
|
||||
|
||||
// Core error interface
|
||||
type Error interface {
|
||||
Error() string
|
||||
Code() string
|
||||
Category() string
|
||||
HTTPStatus() int
|
||||
GRPCCode() codes.Code
|
||||
Metadata() map[string]interface{}
|
||||
Cause() error
|
||||
GetLocalizedMessage(lang string) string
|
||||
WithMetadata(key string, value interface{}) Error
|
||||
WithCause(err error) Error
|
||||
}
|
||||
|
||||
// Metadata type for error context
|
||||
type Metadata map[string]interface{}
|
||||
|
||||
// Core error implementation
|
||||
type AppError struct {
|
||||
code string
|
||||
message string
|
||||
category string
|
||||
httpStatus int
|
||||
grpcCode codes.Code
|
||||
metadata Metadata
|
||||
cause error
|
||||
timestamp time.Time
|
||||
stackTrace []string
|
||||
}
|
||||
|
||||
// New creates a new error
|
||||
func New(message string) Error {
|
||||
return &AppError{
|
||||
code: ErrCodeInternalError,
|
||||
message: message,
|
||||
category: CategoryInternal,
|
||||
httpStatus: http.StatusInternalServerError,
|
||||
grpcCode: codes.Internal,
|
||||
metadata: make(Metadata),
|
||||
timestamp: time.Now(),
|
||||
stackTrace: captureStackTrace(),
|
||||
}
|
||||
}
|
||||
|
||||
// NewWithCode creates a new error with code
|
||||
func NewWithCode(code, message string) Error {
|
||||
info := GetErrorInfo(code)
|
||||
if info.Code == "" {
|
||||
info = ErrorInfo{
|
||||
Code: code,
|
||||
Category: CategoryInternal,
|
||||
HTTPStatus: http.StatusInternalServerError,
|
||||
GRPCCode: codes.Internal,
|
||||
}
|
||||
}
|
||||
|
||||
return &AppError{
|
||||
code: code,
|
||||
message: message,
|
||||
category: info.Category,
|
||||
httpStatus: info.HTTPStatus,
|
||||
grpcCode: info.GRPCCode,
|
||||
metadata: make(Metadata),
|
||||
timestamp: time.Now(),
|
||||
stackTrace: captureStackTrace(),
|
||||
}
|
||||
}
|
||||
|
||||
// NewWithMetadata creates a new error with metadata
|
||||
func NewWithMetadata(code, message string, metadata Metadata) Error {
|
||||
err := NewWithCode(code, message)
|
||||
err.(*AppError).metadata = metadata
|
||||
return err
|
||||
}
|
||||
|
||||
// NewWithType creates a new error with custom type
|
||||
func NewWithType(errorType string, metadata Metadata) Error {
|
||||
template := GetErrorTemplate(errorType)
|
||||
if template == nil {
|
||||
return NewWithMetadata(ErrCodeInternalError, "Unknown error type", metadata)
|
||||
}
|
||||
|
||||
err := &AppError{
|
||||
code: template.Code,
|
||||
message: template.Message,
|
||||
category: template.Category,
|
||||
httpStatus: template.HTTPStatus,
|
||||
grpcCode: template.GRPCCode,
|
||||
metadata: make(Metadata),
|
||||
timestamp: time.Now(),
|
||||
stackTrace: captureStackTrace(),
|
||||
}
|
||||
|
||||
for k, v := range metadata {
|
||||
err.metadata[k] = v
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Wrap wraps an existing error
|
||||
func Wrap(err error, code, message string) Error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if IsAppError(err) {
|
||||
return err.(Error)
|
||||
}
|
||||
|
||||
appErr := NewWithCode(code, message)
|
||||
appErr.(*AppError).cause = err
|
||||
return appErr
|
||||
}
|
||||
|
||||
// FromError converts any error to AppError
|
||||
func FromError(err error) Error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if appErr, ok := err.(Error); ok {
|
||||
return appErr
|
||||
}
|
||||
|
||||
return Wrap(err, ErrCodeInternalError, err.Error())
|
||||
}
|
||||
|
||||
// IsAppError checks if error is AppError
|
||||
func IsAppError(err error) bool {
|
||||
_, ok := err.(Error)
|
||||
return ok
|
||||
}
|
||||
|
||||
// Error implements error interface
|
||||
func (e *AppError) Error() string {
|
||||
if e.cause != nil {
|
||||
return fmt.Sprintf("%s: %v", e.message, e.cause)
|
||||
}
|
||||
return e.message
|
||||
}
|
||||
|
||||
// Code returns error code
|
||||
func (e *AppError) Code() string {
|
||||
return e.code
|
||||
}
|
||||
|
||||
// Category returns error category
|
||||
func (e *AppError) Category() string {
|
||||
return e.category
|
||||
}
|
||||
|
||||
// HTTPStatus returns HTTP status code
|
||||
func (e *AppError) HTTPStatus() int {
|
||||
return e.httpStatus
|
||||
}
|
||||
|
||||
// GRPCCode returns gRPC code
|
||||
func (e *AppError) GRPCCode() codes.Code {
|
||||
return e.grpcCode
|
||||
}
|
||||
|
||||
// Metadata returns error metadata
|
||||
func (e *AppError) Metadata() map[string]interface{} {
|
||||
return e.metadata
|
||||
}
|
||||
|
||||
// Cause returns underlying error
|
||||
func (e *AppError) Cause() error {
|
||||
return e.cause
|
||||
}
|
||||
|
||||
// Unwrap implements the standard library unwrap interface for Go 1.13+ error chains
|
||||
func (e *AppError) Unwrap() error {
|
||||
return e.cause
|
||||
}
|
||||
|
||||
// GetLocalizedMessage returns localized message
|
||||
func (e *AppError) GetLocalizedMessage(lang string) string {
|
||||
return GetLocalizedMessage(e.code, lang, e.message)
|
||||
}
|
||||
|
||||
// WithMetadata adds metadata to error
|
||||
func (e *AppError) WithMetadata(key string, value interface{}) Error {
|
||||
if e.metadata == nil {
|
||||
e.metadata = make(Metadata)
|
||||
}
|
||||
e.metadata[key] = value
|
||||
return e
|
||||
}
|
||||
|
||||
// WithCause adds cause to error
|
||||
func (e *AppError) WithCause(err error) Error {
|
||||
e.cause = err
|
||||
return e
|
||||
}
|
||||
|
||||
// ToJSON converts error to JSON
|
||||
func (e *AppError) ToJSON() ([]byte, error) {
|
||||
return json.Marshal(map[string]interface{}{
|
||||
"code": e.code,
|
||||
"message": e.message,
|
||||
"category": e.category,
|
||||
"http_status": e.httpStatus,
|
||||
"grpc_code": e.grpcCode.String(),
|
||||
"metadata": e.metadata,
|
||||
"timestamp": e.timestamp,
|
||||
"stack_trace": e.stackTrace,
|
||||
})
|
||||
}
|
||||
|
||||
// captureStackTrace captures current stack trace
|
||||
func captureStackTrace() []string {
|
||||
var stack []string
|
||||
for i := 2; i < 15; i++ {
|
||||
pc, file, line, ok := runtime.Caller(i)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
fn := runtime.FuncForPC(pc)
|
||||
stack = append(stack, fmt.Sprintf("%s:%d %s", file, line, fn.Name()))
|
||||
}
|
||||
return stack
|
||||
}
|
||||
|
||||
// Is checks if error matches target
|
||||
func Is(err, target error) bool {
|
||||
if err == target {
|
||||
return true
|
||||
}
|
||||
|
||||
if appErr, ok := err.(Error); ok {
|
||||
if targetErr, ok := target.(Error); ok {
|
||||
return appErr.Code() == targetErr.Code()
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// As finds the first error in err's chain that matches target
|
||||
func As(err error, target interface{}) bool {
|
||||
if appErr, ok := err.(Error); ok {
|
||||
switch t := target.(type) {
|
||||
case **AppError:
|
||||
*t = appErr.(*AppError)
|
||||
return true
|
||||
case *Error:
|
||||
*t = appErr
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"service/pkg/logger"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// GRPCErrorInterceptor creates unary interceptor for error handling
|
||||
func GRPCUnaryInterceptor() grpc.UnaryServerInterceptor {
|
||||
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
|
||||
resp, err := handler(ctx, req)
|
||||
if err != nil {
|
||||
return nil, HandleGRPCError(err)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
}
|
||||
|
||||
// GRPCStreamInterceptor creates stream interceptor for error handling
|
||||
func GRPCStreamInterceptor() grpc.StreamServerInterceptor {
|
||||
return func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
|
||||
err := handler(srv, ss)
|
||||
if err != nil {
|
||||
return HandleGRPCError(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// HandleGRPCError converts error to gRPC error
|
||||
func HandleGRPCError(err error) error {
|
||||
appErr := FromError(err)
|
||||
if appErr == nil {
|
||||
return status.Error(codes.Internal, "Internal server error")
|
||||
}
|
||||
|
||||
// Log gRPC error
|
||||
LogGRPCError(appErr)
|
||||
|
||||
// Convert to gRPC status
|
||||
return status.Error(appErr.GRPCCode(), appErr.GetLocalizedMessage("en"))
|
||||
}
|
||||
|
||||
// LogGRPCError logs gRPC error
|
||||
func LogGRPCError(err Error) {
|
||||
log := logger.Default()
|
||||
|
||||
log.Error("gRPC error occurred",
|
||||
logger.String("error_code", err.Code()),
|
||||
logger.String("category", err.Category()),
|
||||
logger.String("grpc_code", err.GRPCCode().String()),
|
||||
logger.Any("metadata", err.Metadata()),
|
||||
)
|
||||
}
|
||||
|
||||
// FromGRPCError converts gRPC error to AppError
|
||||
func FromGRPCError(err error) Error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
st, ok := status.FromError(err)
|
||||
if !ok {
|
||||
return NewWithCode(ErrCodeInternalError, err.Error())
|
||||
}
|
||||
|
||||
// Map gRPC codes to error codes
|
||||
code := mapGRPCToErrorCode(st.Code())
|
||||
appErr := NewWithCode(code, st.Message())
|
||||
|
||||
// Add gRPC metadata
|
||||
appErr = appErr.WithMetadata("grpc_code", st.Code().String())
|
||||
|
||||
return appErr
|
||||
}
|
||||
|
||||
// mapGRPCToErrorCode maps gRPC codes to error codes
|
||||
func mapGRPCToErrorCode(grpcCode codes.Code) string {
|
||||
switch grpcCode {
|
||||
case codes.OK:
|
||||
return ""
|
||||
case codes.Canceled:
|
||||
return ErrCodeTimeout
|
||||
case codes.Unknown:
|
||||
return ErrCodeInternalError
|
||||
case codes.InvalidArgument:
|
||||
return ErrCodeValidationFailed
|
||||
case codes.DeadlineExceeded:
|
||||
return ErrCodeTimeout
|
||||
case codes.NotFound:
|
||||
return ErrCodeNotFound
|
||||
case codes.AlreadyExists:
|
||||
return ErrCodeDuplicateEntry
|
||||
case codes.PermissionDenied:
|
||||
return ErrCodeForbidden
|
||||
case codes.ResourceExhausted:
|
||||
return ErrCodeRateLimitExceeded
|
||||
case codes.FailedPrecondition:
|
||||
return ErrCodeBusinessRule
|
||||
case codes.Aborted:
|
||||
return ErrCodeConflict
|
||||
case codes.OutOfRange:
|
||||
return ErrCodeInvalidRange
|
||||
case codes.Unimplemented:
|
||||
return ErrCodeServiceUnavailable
|
||||
case codes.Internal:
|
||||
return ErrCodeInternalError
|
||||
case codes.Unavailable:
|
||||
return ErrCodeExternalError
|
||||
case codes.DataLoss:
|
||||
return ErrCodeInternalError
|
||||
case codes.Unauthenticated:
|
||||
return ErrCodeUnauthorized
|
||||
default:
|
||||
return ErrCodeInternalError
|
||||
}
|
||||
}
|
||||
|
||||
// GRPCMiddlewareConfig configures gRPC middleware
|
||||
type GRPCMiddlewareConfig struct {
|
||||
SkipMethods []string
|
||||
LogAllErrors bool
|
||||
SanitizeErrors bool
|
||||
DefaultLanguage string
|
||||
EnableMetrics bool
|
||||
CustomCodeMapping map[codes.Code]string
|
||||
}
|
||||
|
||||
// DefaultGRPCMiddlewareConfig returns default gRPC configuration
|
||||
func DefaultGRPCMiddlewareConfig() *GRPCMiddlewareConfig {
|
||||
return &GRPCMiddlewareConfig{
|
||||
SkipMethods: []string{"/grpc.health.v1.Health/Check"},
|
||||
LogAllErrors: true,
|
||||
SanitizeErrors: true,
|
||||
DefaultLanguage: "en",
|
||||
EnableMetrics: true,
|
||||
}
|
||||
}
|
||||
|
||||
// GRPCUnaryInterceptorWithConfig creates interceptor with custom configuration
|
||||
func GRPCUnaryInterceptorWithConfig(config *GRPCMiddlewareConfig) grpc.UnaryServerInterceptor {
|
||||
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
|
||||
// Skip specified methods
|
||||
for _, method := range config.SkipMethods {
|
||||
if info.FullMethod == method {
|
||||
return handler(ctx, req)
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := handler(ctx, req)
|
||||
if err != nil {
|
||||
return nil, handleGRPCErrorWithConfig(err, config)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
}
|
||||
|
||||
// handleGRPCErrorWithConfig handles gRPC error with custom configuration
|
||||
func handleGRPCErrorWithConfig(err error, config *GRPCMiddlewareConfig) error {
|
||||
appErr := FromError(err)
|
||||
if appErr == nil {
|
||||
return status.Error(codes.Internal, "Internal server error")
|
||||
}
|
||||
|
||||
// Apply custom code mapping
|
||||
if customCode, exists := config.CustomCodeMapping[appErr.GRPCCode()]; exists {
|
||||
appErr = NewWithCode(customCode, appErr.Error())
|
||||
}
|
||||
|
||||
// Sanitize error if needed
|
||||
if config.SanitizeErrors {
|
||||
appErr = sanitizeError(appErr).(Error)
|
||||
}
|
||||
|
||||
// Log error
|
||||
if config.LogAllErrors {
|
||||
LogGRPCError(appErr)
|
||||
}
|
||||
|
||||
return status.Error(appErr.GRPCCode(), appErr.GetLocalizedMessage(config.DefaultLanguage))
|
||||
}
|
||||
|
||||
// ConvertToHTTPStatus converts gRPC code to HTTP status
|
||||
func ConvertToHTTPStatus(grpcCode codes.Code) int {
|
||||
switch grpcCode {
|
||||
case codes.OK:
|
||||
return http.StatusOK
|
||||
case codes.Canceled:
|
||||
return http.StatusRequestTimeout
|
||||
case codes.Unknown:
|
||||
return http.StatusInternalServerError
|
||||
case codes.InvalidArgument:
|
||||
return http.StatusBadRequest
|
||||
case codes.DeadlineExceeded:
|
||||
return http.StatusRequestTimeout
|
||||
case codes.NotFound:
|
||||
return http.StatusNotFound
|
||||
case codes.AlreadyExists:
|
||||
return http.StatusConflict
|
||||
case codes.PermissionDenied:
|
||||
return http.StatusForbidden
|
||||
case codes.ResourceExhausted:
|
||||
return http.StatusTooManyRequests
|
||||
case codes.FailedPrecondition:
|
||||
return http.StatusBadRequest
|
||||
case codes.Aborted:
|
||||
return http.StatusConflict
|
||||
case codes.OutOfRange:
|
||||
return http.StatusBadRequest
|
||||
case codes.Unimplemented:
|
||||
return http.StatusNotImplemented
|
||||
case codes.Internal:
|
||||
return http.StatusInternalServerError
|
||||
case codes.Unavailable:
|
||||
return http.StatusServiceUnavailable
|
||||
case codes.DataLoss:
|
||||
return http.StatusInternalServerError
|
||||
case codes.Unauthenticated:
|
||||
return http.StatusUnauthorized
|
||||
default:
|
||||
return http.StatusInternalServerError
|
||||
}
|
||||
}
|
||||
|
||||
// ConvertToGRPCCode converts HTTP status to gRPC code
|
||||
func ConvertToGRPCCode(httpStatus int) codes.Code {
|
||||
switch httpStatus {
|
||||
case http.StatusOK:
|
||||
return codes.OK
|
||||
case http.StatusBadRequest:
|
||||
return codes.InvalidArgument
|
||||
case http.StatusUnauthorized:
|
||||
return codes.Unauthenticated
|
||||
case http.StatusForbidden:
|
||||
return codes.PermissionDenied
|
||||
case http.StatusNotFound:
|
||||
return codes.NotFound
|
||||
case http.StatusConflict:
|
||||
return codes.AlreadyExists
|
||||
case http.StatusTooManyRequests:
|
||||
return codes.ResourceExhausted
|
||||
case http.StatusRequestTimeout:
|
||||
return codes.DeadlineExceeded
|
||||
case http.StatusNotImplemented:
|
||||
return codes.Unimplemented
|
||||
case http.StatusInternalServerError:
|
||||
return codes.Internal
|
||||
case http.StatusServiceUnavailable:
|
||||
return codes.Unavailable
|
||||
case http.StatusBadGateway:
|
||||
return codes.Unavailable
|
||||
default:
|
||||
return codes.Unknown
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"service/pkg/logger"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// HTTPErrorResponse represents standardized HTTP error response
|
||||
type HTTPErrorResponse struct {
|
||||
Error struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Details map[string]interface{} `json:"details,omitempty"`
|
||||
RequestID string `json:"request_id,omitempty"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
Retryable bool `json:"retryable,omitempty"`
|
||||
} `json:"error"`
|
||||
Meta struct {
|
||||
HTTPStatus int `json:"http_status"`
|
||||
Category string `json:"category"`
|
||||
} `json:"meta,omitempty"`
|
||||
}
|
||||
|
||||
// HTTPMiddleware creates error handling middleware for Gin
|
||||
func HTTPMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Next()
|
||||
|
||||
// Check if there are any errors
|
||||
if len(c.Errors) > 0 {
|
||||
// Get the last error
|
||||
err := c.Errors.Last().Err
|
||||
HandleHTTPError(c, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// HandleHTTPError handles HTTP error response
|
||||
func HandleHTTPError(c *gin.Context, err error) {
|
||||
appErr := FromError(err)
|
||||
if appErr == nil {
|
||||
appErr = NewWithCode(ErrCodeInternalError, "Unknown error")
|
||||
}
|
||||
|
||||
// Create error response
|
||||
response := &HTTPErrorResponse{}
|
||||
response.Error.Code = appErr.Code()
|
||||
response.Error.Message = appErr.GetLocalizedMessage(getLanguageFromContext(c))
|
||||
response.Error.Details = appErr.Metadata()
|
||||
response.Error.RequestID = c.GetString("request_id")
|
||||
response.Error.Timestamp = appErr.Metadata()["timestamp"].(string)
|
||||
response.Error.Retryable = IsRetryable(appErr.Code())
|
||||
response.Meta.HTTPStatus = appErr.HTTPStatus()
|
||||
response.Meta.Category = appErr.Category()
|
||||
|
||||
// Log error
|
||||
LogHTTPError(c, appErr)
|
||||
|
||||
// Send response
|
||||
c.JSON(appErr.HTTPStatus(), response)
|
||||
}
|
||||
|
||||
// getLanguageFromContext extracts language from context
|
||||
func getLanguageFromContext(c *gin.Context) string {
|
||||
// Try Accept-Language header
|
||||
lang := c.GetHeader("Accept-Language")
|
||||
if lang != "" {
|
||||
return parseAcceptLanguage(lang)
|
||||
}
|
||||
|
||||
// Try query parameter
|
||||
lang = c.Query("lang")
|
||||
if lang != "" {
|
||||
return lang
|
||||
}
|
||||
|
||||
// Default to English
|
||||
return "en"
|
||||
}
|
||||
|
||||
// parseAcceptLanguage parses Accept-Language header
|
||||
func parseAcceptLanguage(header string) string {
|
||||
// Simple parsing - take first language
|
||||
for idx := 0; idx < len(header); idx++ {
|
||||
if header[idx] == ',' || header[idx] == ';' {
|
||||
return header[:idx]
|
||||
}
|
||||
}
|
||||
return header
|
||||
}
|
||||
|
||||
// LogHTTPError logs HTTP error with context
|
||||
func LogHTTPError(c *gin.Context, err Error) {
|
||||
log := logger.Default()
|
||||
|
||||
log.Error("HTTP error occurred",
|
||||
logger.String("error_code", err.Code()),
|
||||
logger.String("category", err.Category()),
|
||||
logger.Int("http_status", err.HTTPStatus()),
|
||||
logger.Any("metadata", err.Metadata()),
|
||||
logger.String("path", c.Request.URL.Path),
|
||||
logger.String("method", c.Request.Method),
|
||||
)
|
||||
}
|
||||
|
||||
// ValidationErrorHandler handles validation errors specifically
|
||||
func ValidationErrorHandler(c *gin.Context, err error) {
|
||||
appErr := FromError(err)
|
||||
if appErr == nil {
|
||||
appErr = NewWithCode(ErrCodeValidationFailed, err.Error())
|
||||
}
|
||||
|
||||
// Enhance validation errors with field details
|
||||
if appErr.Category() == CategoryValidation {
|
||||
if details, ok := appErr.Metadata()["validation_errors"]; ok {
|
||||
response := gin.H{
|
||||
"error": gin.H{
|
||||
"code": appErr.Code(),
|
||||
"message": appErr.GetLocalizedMessage(getLanguageFromContext(c)),
|
||||
"fields": details,
|
||||
},
|
||||
}
|
||||
c.JSON(http.StatusBadRequest, response)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
HandleHTTPError(c, appErr)
|
||||
}
|
||||
|
||||
// PanicHandler handles panics
|
||||
func PanicHandler(c *gin.Context, recovered interface{}) {
|
||||
var err error
|
||||
|
||||
switch x := recovered.(type) {
|
||||
case string:
|
||||
err = NewWithCode(ErrCodeInternalError, x)
|
||||
case error:
|
||||
err = FromError(x)
|
||||
default:
|
||||
err = NewWithCode(ErrCodeUnexpectedError, fmt.Sprintf("Unknown panic: %v", x))
|
||||
}
|
||||
|
||||
HandleHTTPError(c, err)
|
||||
}
|
||||
|
||||
// WriteErrorResponse writes error response directly
|
||||
func WriteErrorResponse(w http.ResponseWriter, err error) {
|
||||
appErr := FromError(err)
|
||||
if appErr == nil {
|
||||
appErr = NewWithCode(ErrCodeInternalError, "Unknown error")
|
||||
}
|
||||
|
||||
response := &HTTPErrorResponse{}
|
||||
response.Error.Code = appErr.Code()
|
||||
response.Error.Message = appErr.GetLocalizedMessage("en")
|
||||
response.Error.Details = appErr.Metadata()
|
||||
response.Error.Timestamp = appErr.Metadata()["timestamp"].(string)
|
||||
response.Error.Retryable = IsRetryable(appErr.Code())
|
||||
response.Meta.HTTPStatus = appErr.HTTPStatus()
|
||||
response.Meta.Category = appErr.Category()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(appErr.HTTPStatus())
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// sanitizeError removes sensitive information from errors
|
||||
func sanitizeError(err error) error {
|
||||
appErr := FromError(err)
|
||||
if appErr == nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Remove sensitive metadata
|
||||
sanitized := make(Metadata)
|
||||
for k, v := range appErr.Metadata() {
|
||||
if !isSensitiveField(k) {
|
||||
sanitized[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
return NewWithMetadata(appErr.Code(), appErr.Error(), sanitized)
|
||||
}
|
||||
|
||||
// isSensitiveField checks if field contains sensitive information
|
||||
func isSensitiveField(field string) bool {
|
||||
sensitiveFields := []string{"password", "token", "secret", "key", "auth"}
|
||||
for _, sensitive := range sensitiveFields {
|
||||
if strings.Contains(strings.ToLower(field), sensitive) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// generateRequestID generates a unique request ID
|
||||
func generateRequestID() string {
|
||||
return fmt.Sprintf("req_%d", time.Now().UnixNano())
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// MessageStore stores localized messages
|
||||
type MessageStore struct {
|
||||
messages map[string]map[string]string // language -> code -> message
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
var (
|
||||
messageStore = &MessageStore{
|
||||
messages: make(map[string]map[string]string),
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Initialize default messages
|
||||
initializeDefaultMessages()
|
||||
}
|
||||
|
||||
// initializeDefaultMessages sets up default error messages
|
||||
func initializeDefaultMessages() {
|
||||
// English messages
|
||||
englishMessages := map[string]string{
|
||||
ErrCodeValidationFailed: "Validation failed",
|
||||
ErrCodeInvalidInput: "Invalid input provided",
|
||||
ErrCodeMissingField: "Required field is missing",
|
||||
ErrCodeInvalidFormat: "Invalid format",
|
||||
ErrCodeInvalidLength: "Invalid length",
|
||||
ErrCodeInvalidRange: "Value out of range",
|
||||
ErrCodeNotFound: "Resource not found",
|
||||
ErrCodeUserNotFound: "User not found",
|
||||
ErrCodeResourceNotFound: "Resource not found",
|
||||
ErrCodeDataNotFound: "Data not found",
|
||||
ErrCodeUnauthorized: "Unauthorized access",
|
||||
ErrCodeInvalidToken: "Invalid token provided",
|
||||
ErrCodeTokenExpired: "Token has expired",
|
||||
ErrCodeInvalidCredentials: "Invalid credentials",
|
||||
ErrCodeForbidden: "Access forbidden",
|
||||
ErrCodeInsufficientRights: "Insufficient permissions",
|
||||
ErrCodeAccessDenied: "Access denied",
|
||||
ErrCodeConflict: "Conflict occurred",
|
||||
ErrCodeDuplicateEntry: "Duplicate entry",
|
||||
ErrCodeResourceLocked: "Resource is locked",
|
||||
ErrCodeConcurrentUpdate: "Concurrent update detected",
|
||||
ErrCodeRateLimitExceeded: "Rate limit exceeded",
|
||||
ErrCodeTooManyRequests: "Too many requests",
|
||||
ErrCodeQuotaExceeded: "Quota exceeded",
|
||||
ErrCodeInternalError: "Internal server error",
|
||||
ErrCodeUnexpectedError: "An unexpected error occurred",
|
||||
ErrCodeServiceUnavailable: "Service is unavailable",
|
||||
ErrCodeConfigurationError: "Configuration error",
|
||||
ErrCodeExternalError: "External service error",
|
||||
ErrCodeThirdPartyError: "Third party service error",
|
||||
ErrCodeAPIError: "API error occurred",
|
||||
ErrCodeBusinessRule: "Business rule violation",
|
||||
ErrCodeInsufficientBalance: "Insufficient balance",
|
||||
ErrCodeAccountSuspended: "Account suspended",
|
||||
ErrCodeTimeout: "Request timeout",
|
||||
ErrCodeRequestTimeout: "Request timeout",
|
||||
ErrCodeConnectionTimeout: "Connection timeout",
|
||||
ErrCodeNetworkError: "Network error",
|
||||
ErrCodeConnectionFailed: "Connection failed",
|
||||
ErrCodeConnectionLost: "Connection lost",
|
||||
ErrCodeDatabaseError: "Database error",
|
||||
ErrCodeQueryFailed: "Query execution failed",
|
||||
ErrCodeTransactionFailed: "Transaction failed",
|
||||
}
|
||||
|
||||
// Indonesian messages
|
||||
indonesianMessages := map[string]string{
|
||||
ErrCodeValidationFailed: "Validasi gagal",
|
||||
ErrCodeInvalidInput: "Input tidak valid",
|
||||
ErrCodeMissingField: "Field wajib tidak ada",
|
||||
ErrCodeInvalidFormat: "Format tidak valid",
|
||||
ErrCodeInvalidLength: "Panjang tidak valid",
|
||||
ErrCodeInvalidRange: "Nilai di luar jangkauan",
|
||||
ErrCodeNotFound: "Sumber daya tidak ditemukan",
|
||||
ErrCodeUserNotFound: "Pengguna tidak ditemukan",
|
||||
ErrCodeResourceNotFound: "Sumber daya tidak ditemukan",
|
||||
ErrCodeDataNotFound: "Data tidak ditemukan",
|
||||
ErrCodeUnauthorized: "Akses tidak sah",
|
||||
ErrCodeInvalidToken: "Token tidak valid",
|
||||
ErrCodeTokenExpired: "Token telah kadaluarsa",
|
||||
ErrCodeInvalidCredentials: "Kredensial tidak valid",
|
||||
ErrCodeForbidden: "Akses dilarang",
|
||||
ErrCodeInsufficientRights: "Izin tidak mencukupi",
|
||||
ErrCodeAccessDenied: "Akses ditolak",
|
||||
ErrCodeConflict: "Terjadi konflik",
|
||||
ErrCodeDuplicateEntry: "Entri duplikat",
|
||||
ErrCodeResourceLocked: "Sumber daya terkunci",
|
||||
ErrCodeConcurrentUpdate: "Pembaruan bersamaan terdeteksi",
|
||||
ErrCodeRateLimitExceeded: "Batas laju terlampaui",
|
||||
ErrCodeTooManyRequests: "Terlalu banyak permintaan",
|
||||
ErrCodeQuotaExceeded: "Kuota terlampaui",
|
||||
ErrCodeInternalError: "Kesalahan server internal",
|
||||
ErrCodeUnexpectedError: "Terjadi kesalahan tak terduga",
|
||||
ErrCodeServiceUnavailable: "Layanan tidak tersedia",
|
||||
ErrCodeConfigurationError: "Kesalahan konfigurasi",
|
||||
ErrCodeExternalError: "Kesalahan layanan eksternal",
|
||||
ErrCodeThirdPartyError: "Kesalahan layanan pihak ketiga",
|
||||
ErrCodeAPIError: "Terjadi kesalahan API",
|
||||
ErrCodeBusinessRule: "Pelanggaran aturan bisnis",
|
||||
ErrCodeInsufficientBalance: "Saldo tidak mencukupi",
|
||||
ErrCodeAccountSuspended: "Akun ditangguhkan",
|
||||
ErrCodeTimeout: "Permintaan timeout",
|
||||
ErrCodeRequestTimeout: "Permintaan timeout",
|
||||
ErrCodeConnectionTimeout: "Koneksi timeout",
|
||||
ErrCodeNetworkError: "Kesalahan jaringan",
|
||||
ErrCodeConnectionFailed: "Koneksi gagal",
|
||||
ErrCodeConnectionLost: "Koneksi terputus",
|
||||
ErrCodeDatabaseError: "Kesalahan database",
|
||||
ErrCodeQueryFailed: "Eksekusi query gagal",
|
||||
ErrCodeTransactionFailed: "Transaksi gagal",
|
||||
}
|
||||
|
||||
// Add messages to store
|
||||
SetMessages("en", englishMessages)
|
||||
SetMessages("id", indonesianMessages)
|
||||
}
|
||||
|
||||
// SetMessages sets messages for a language
|
||||
func SetMessages(language string, messages map[string]string) {
|
||||
messageStore.mu.Lock()
|
||||
defer messageStore.mu.Unlock()
|
||||
|
||||
if messageStore.messages[language] == nil {
|
||||
messageStore.messages[language] = make(map[string]string)
|
||||
}
|
||||
|
||||
for code, message := range messages {
|
||||
messageStore.messages[language][code] = message
|
||||
}
|
||||
}
|
||||
|
||||
// SetMessage sets a single message for a language
|
||||
func SetMessage(language, code, message string) {
|
||||
messageStore.mu.Lock()
|
||||
defer messageStore.mu.Unlock()
|
||||
|
||||
if messageStore.messages[language] == nil {
|
||||
messageStore.messages[language] = make(map[string]string)
|
||||
}
|
||||
|
||||
messageStore.messages[language][code] = message
|
||||
}
|
||||
|
||||
// GetLocalizedMessage returns localized message for error code
|
||||
func GetLocalizedMessage(code, language, defaultMessage string) string {
|
||||
messageStore.mu.RLock()
|
||||
defer messageStore.mu.RUnlock()
|
||||
|
||||
// Try specific language
|
||||
if langMessages, exists := messageStore.messages[language]; exists {
|
||||
if message, exists := langMessages[code]; exists {
|
||||
return message
|
||||
}
|
||||
}
|
||||
|
||||
// Try English as fallback
|
||||
if langMessages, exists := messageStore.messages["en"]; exists {
|
||||
if message, exists := langMessages[code]; exists {
|
||||
return message
|
||||
}
|
||||
}
|
||||
|
||||
// Return default message
|
||||
return defaultMessage
|
||||
}
|
||||
|
||||
// GetSupportedLanguages returns all supported languages
|
||||
func GetSupportedLanguages() []string {
|
||||
messageStore.mu.RLock()
|
||||
defer messageStore.mu.RUnlock()
|
||||
|
||||
languages := make([]string, 0, len(messageStore.messages))
|
||||
for lang := range messageStore.messages {
|
||||
languages = append(languages, lang)
|
||||
}
|
||||
return languages
|
||||
}
|
||||
|
||||
// LoadMessagesFromJSON loads messages from JSON file
|
||||
func LoadMessagesFromJSON(language, jsonData string) error {
|
||||
var messages map[string]string
|
||||
if err := json.Unmarshal([]byte(jsonData), &messages); err != nil {
|
||||
return fmt.Errorf("failed to parse JSON messages: %w", err)
|
||||
}
|
||||
|
||||
SetMessages(language, messages)
|
||||
return nil
|
||||
}
|
||||
|
||||
// FormatMessage formats message with parameters
|
||||
func FormatMessage(template string, params map[string]interface{}) string {
|
||||
result := template
|
||||
for key, value := range params {
|
||||
placeholder := fmt.Sprintf("{{%s}}", key)
|
||||
result = strings.ReplaceAll(result, placeholder, fmt.Sprintf("%v", value))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// GetLocalizedMessageWithParams returns localized message with parameters
|
||||
func GetLocalizedMessageWithParams(code, language, defaultMessage string, params map[string]interface{}) string {
|
||||
message := GetLocalizedMessage(code, language, defaultMessage)
|
||||
if params != nil {
|
||||
message = FormatMessage(message, params)
|
||||
}
|
||||
return message
|
||||
}
|
||||
|
||||
// MessageBuilder builds localized messages
|
||||
type MessageBuilder struct {
|
||||
language string
|
||||
code string
|
||||
params map[string]interface{}
|
||||
}
|
||||
|
||||
// NewMessageBuilder creates a new message builder.
|
||||
// Parameter 'lang' dianjurkan untuk dikirim berdasarkan context HTTP request
|
||||
// misalnya dari header Accept-Language dari klien per request.
|
||||
func NewMessageBuilder(lang string) *MessageBuilder {
|
||||
if lang == "" {
|
||||
lang = "en" // Fallback language default
|
||||
}
|
||||
return &MessageBuilder{
|
||||
language: lang,
|
||||
params: make(map[string]interface{}),
|
||||
}
|
||||
}
|
||||
|
||||
// Language sets language
|
||||
func (mb *MessageBuilder) Language(lang string) *MessageBuilder {
|
||||
mb.language = lang
|
||||
return mb
|
||||
}
|
||||
|
||||
// Code sets error code
|
||||
func (mb *MessageBuilder) Code(code string) *MessageBuilder {
|
||||
mb.code = code
|
||||
return mb
|
||||
}
|
||||
|
||||
// Param adds parameter
|
||||
func (mb *MessageBuilder) Param(key string, value interface{}) *MessageBuilder {
|
||||
mb.params[key] = value
|
||||
return mb
|
||||
}
|
||||
|
||||
// Params adds multiple parameters
|
||||
func (mb *MessageBuilder) Params(params map[string]interface{}) *MessageBuilder {
|
||||
for k, v := range params {
|
||||
mb.params[k] = v
|
||||
}
|
||||
return mb
|
||||
}
|
||||
|
||||
// Build builds the final message
|
||||
func (mb *MessageBuilder) Build(defaultMessage string) string {
|
||||
return GetLocalizedMessageWithParams(mb.code, mb.language, defaultMessage, mb.params)
|
||||
}
|
||||
|
||||
// ValidationMessages provides common validation messages
|
||||
type ValidationMessages struct {
|
||||
Required string
|
||||
Invalid string
|
||||
TooShort string
|
||||
TooLong string
|
||||
InvalidEmail string
|
||||
InvalidPhone string
|
||||
}
|
||||
|
||||
// GetValidationMessages returns validation messages for language
|
||||
func GetValidationMessages(language string) ValidationMessages {
|
||||
return ValidationMessages{
|
||||
Required: GetLocalizedMessage("VALIDATION_REQUIRED", language, "This field is required"),
|
||||
Invalid: GetLocalizedMessage("VALIDATION_INVALID", language, "Invalid value"),
|
||||
TooShort: GetLocalizedMessage("VALIDATION_TOO_SHORT", language, "Value is too short"),
|
||||
TooLong: GetLocalizedMessage("VALIDATION_TOO_LONG", language, "Value is too long"),
|
||||
InvalidEmail: GetLocalizedMessage("VALIDATION_INVALID_EMAIL", language, "Invalid email format"),
|
||||
InvalidPhone: GetLocalizedMessage("VALIDATION_INVALID_PHONE", language, "Invalid phone format"),
|
||||
}
|
||||
}
|
||||
|
||||
// AddValidationMessages adds validation messages for language
|
||||
func AddValidationMessages(language string, messages ValidationMessages) {
|
||||
SetMessage(language, "VALIDATION_REQUIRED", messages.Required)
|
||||
SetMessage(language, "VALIDATION_INVALID", messages.Invalid)
|
||||
SetMessage(language, "VALIDATION_TOO_SHORT", messages.TooShort)
|
||||
SetMessage(language, "VALIDATION_TOO_LONG", messages.TooLong)
|
||||
SetMessage(language, "VALIDATION_INVALID_EMAIL", messages.InvalidEmail)
|
||||
SetMessage(language, "VALIDATION_INVALID_PHONE", messages.InvalidPhone)
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Metrics holds error metrics
|
||||
type Metrics struct {
|
||||
TotalErrors int
|
||||
ErrorsByCode map[string]int
|
||||
ErrorsByCategory map[string]int
|
||||
}
|
||||
|
||||
// ErrorMetrics represents error metrics for external use
|
||||
type ErrorMetrics struct {
|
||||
TotalErrors int `json:"total_errors"`
|
||||
ErrorsByCode map[string]int `json:"errors_by_code"`
|
||||
ErrorsByCategory map[string]int `json:"errors_by_category"`
|
||||
}
|
||||
|
||||
var (
|
||||
globalMetrics = &Metrics{
|
||||
ErrorsByCode: make(map[string]int),
|
||||
ErrorsByCategory: make(map[string]int),
|
||||
}
|
||||
metricsMutex sync.RWMutex
|
||||
)
|
||||
|
||||
// RecordError records an error in metrics
|
||||
func RecordError(err Error) {
|
||||
metricsMutex.Lock()
|
||||
defer metricsMutex.Unlock()
|
||||
|
||||
globalMetrics.TotalErrors++
|
||||
globalMetrics.ErrorsByCode[err.Code()]++
|
||||
globalMetrics.ErrorsByCategory[err.Category()]++
|
||||
}
|
||||
|
||||
// GetMetrics returns a copy of current metrics
|
||||
func GetMetrics() *Metrics {
|
||||
metricsMutex.RLock()
|
||||
defer metricsMutex.RUnlock()
|
||||
|
||||
// Create a deep copy
|
||||
copy := &Metrics{
|
||||
TotalErrors: globalMetrics.TotalErrors,
|
||||
ErrorsByCode: make(map[string]int),
|
||||
ErrorsByCategory: make(map[string]int),
|
||||
}
|
||||
|
||||
for k, v := range globalMetrics.ErrorsByCode {
|
||||
copy.ErrorsByCode[k] = v
|
||||
}
|
||||
|
||||
for k, v := range globalMetrics.ErrorsByCategory {
|
||||
copy.ErrorsByCategory[k] = v
|
||||
}
|
||||
|
||||
return copy
|
||||
}
|
||||
@@ -0,0 +1,441 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"service/pkg/logger"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// ErrorMiddlewareConfig configures error middleware
|
||||
type ErrorMiddlewareConfig struct {
|
||||
// SkipRoutes defines routes to skip error handling
|
||||
SkipRoutes []string
|
||||
|
||||
// CustomHandlers defines custom error handlers
|
||||
CustomHandlers map[string]ErrorHandlerFunc
|
||||
|
||||
// LogAllErrors enables logging all errors
|
||||
LogAllErrors bool
|
||||
|
||||
// SanitizeErrors enables error sanitization
|
||||
SanitizeErrors bool
|
||||
|
||||
// DefaultLanguage sets default language for error messages
|
||||
DefaultLanguage string
|
||||
|
||||
// RequestIDHeader sets header name for request ID
|
||||
RequestIDHeader string
|
||||
|
||||
// EnableMetrics enables error metrics collection
|
||||
EnableMetrics bool
|
||||
|
||||
// Timeout sets timeout for error handling
|
||||
Timeout time.Duration
|
||||
|
||||
// RecoveryEnabled enables panic recovery
|
||||
RecoveryEnabled bool
|
||||
|
||||
// CircuitBreakerEnabled enables circuit breaker
|
||||
CircuitBreakerEnabled bool
|
||||
}
|
||||
|
||||
// ErrorHandlerFunc handles errors
|
||||
type ErrorHandlerFunc func(*gin.Context, error)
|
||||
|
||||
// DefaultMiddlewareConfig returns default configuration
|
||||
func DefaultMiddlewareConfig() *ErrorMiddlewareConfig {
|
||||
return &ErrorMiddlewareConfig{
|
||||
SkipRoutes: []string{"/health", "/metrics", "/ping"},
|
||||
LogAllErrors: true,
|
||||
SanitizeErrors: true,
|
||||
DefaultLanguage: "en",
|
||||
RequestIDHeader: "X-Request-ID",
|
||||
EnableMetrics: true,
|
||||
Timeout: 30 * time.Second,
|
||||
RecoveryEnabled: true,
|
||||
CircuitBreakerEnabled: false,
|
||||
}
|
||||
}
|
||||
|
||||
// ErrorMiddleware creates Gin middleware for error handling
|
||||
func ErrorMiddleware(config ...*ErrorMiddlewareConfig) gin.HandlerFunc {
|
||||
var cfg *ErrorMiddlewareConfig
|
||||
if len(config) > 0 {
|
||||
cfg = config[0]
|
||||
} else {
|
||||
cfg = DefaultMiddlewareConfig()
|
||||
}
|
||||
|
||||
return func(c *gin.Context) {
|
||||
// Skip specified routes
|
||||
for _, route := range cfg.SkipRoutes {
|
||||
if c.Request.URL.Path == route {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Generate request ID if not present
|
||||
requestID := c.GetHeader(cfg.RequestIDHeader)
|
||||
if requestID == "" {
|
||||
requestID = generateRequestID()
|
||||
}
|
||||
c.Set("request_id", requestID)
|
||||
|
||||
// Set language
|
||||
lang := getLanguageFromContext(c)
|
||||
if lang == "" {
|
||||
lang = cfg.DefaultLanguage
|
||||
}
|
||||
c.Set("language", lang)
|
||||
|
||||
// Create context with timeout
|
||||
if cfg.Timeout > 0 {
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), cfg.Timeout)
|
||||
defer cancel()
|
||||
c.Request = c.Request.WithContext(ctx)
|
||||
}
|
||||
|
||||
// Process request
|
||||
c.Next()
|
||||
|
||||
// Handle errors
|
||||
if len(c.Errors) > 0 {
|
||||
handleErrors(c, c.Errors.Last().Err, cfg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RecoveryMiddleware creates recovery middleware
|
||||
func RecoveryMiddleware(config ...*ErrorMiddlewareConfig) gin.HandlerFunc {
|
||||
var cfg *ErrorMiddlewareConfig
|
||||
if len(config) > 0 {
|
||||
cfg = config[0]
|
||||
} else {
|
||||
cfg = DefaultMiddlewareConfig()
|
||||
}
|
||||
|
||||
return gin.CustomRecovery(func(c *gin.Context, recovered interface{}) {
|
||||
var err error
|
||||
|
||||
switch x := recovered.(type) {
|
||||
case string:
|
||||
err = NewWithCode(ErrCodeInternalError, x)
|
||||
case error:
|
||||
err = FromError(x)
|
||||
default:
|
||||
err = NewWithCode(ErrCodeUnexpectedError,
|
||||
fmt.Sprintf("Unknown panic: %v", x))
|
||||
}
|
||||
|
||||
handleErrors(c, err, cfg)
|
||||
})
|
||||
}
|
||||
|
||||
// CombinedMiddleware creates combined error and recovery middleware
|
||||
func CombinedMiddleware(config ...*ErrorMiddlewareConfig) gin.HandlerFunc {
|
||||
cfg := DefaultMiddlewareConfig()
|
||||
if len(config) > 0 {
|
||||
cfg = config[0]
|
||||
}
|
||||
|
||||
return func(c *gin.Context) {
|
||||
// Skip specified routes
|
||||
for _, route := range cfg.SkipRoutes {
|
||||
if c.Request.URL.Path == route {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Generate request ID if not present
|
||||
requestID := c.GetHeader(cfg.RequestIDHeader)
|
||||
if requestID == "" {
|
||||
requestID = generateRequestID()
|
||||
}
|
||||
c.Set("request_id", requestID)
|
||||
|
||||
// Set language
|
||||
lang := getLanguageFromContext(c)
|
||||
if lang == "" {
|
||||
lang = cfg.DefaultLanguage
|
||||
}
|
||||
c.Set("language", lang)
|
||||
|
||||
// Create context with timeout
|
||||
if cfg.Timeout > 0 {
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), cfg.Timeout)
|
||||
defer cancel()
|
||||
c.Request = c.Request.WithContext(ctx)
|
||||
}
|
||||
|
||||
// Process request with recovery
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
var err error
|
||||
|
||||
switch x := r.(type) {
|
||||
case string:
|
||||
err = NewWithCode(ErrCodeInternalError, x)
|
||||
case error:
|
||||
err = FromError(x)
|
||||
default:
|
||||
err = NewWithCode(ErrCodeUnexpectedError,
|
||||
fmt.Sprintf("Unknown panic: %v", x))
|
||||
}
|
||||
|
||||
handleErrors(c, err, cfg)
|
||||
}
|
||||
}()
|
||||
|
||||
c.Next()
|
||||
|
||||
// Handle errors
|
||||
if len(c.Errors) > 0 {
|
||||
handleErrors(c, c.Errors.Last().Err, cfg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleErrors handles errors with configuration
|
||||
func handleErrors(c *gin.Context, err error, cfg *ErrorMiddlewareConfig) {
|
||||
appErr := FromError(err)
|
||||
if appErr == nil {
|
||||
appErr = NewWithCode(ErrCodeInternalError, "Unknown error")
|
||||
}
|
||||
|
||||
// Apply custom handlers
|
||||
if handler, exists := cfg.CustomHandlers[appErr.Code()]; exists {
|
||||
handler(c, appErr)
|
||||
return
|
||||
}
|
||||
|
||||
// Apply registered handlers
|
||||
appErr = ApplyHandlers(appErr)
|
||||
|
||||
// Sanitize error if needed
|
||||
if cfg.SanitizeErrors {
|
||||
appErr = sanitizeError(appErr).(Error)
|
||||
}
|
||||
|
||||
// Record metrics
|
||||
if cfg.EnableMetrics {
|
||||
RecordError(appErr)
|
||||
}
|
||||
|
||||
// Log error
|
||||
if cfg.LogAllErrors {
|
||||
log := logger.Default()
|
||||
log.Error("HTTP error occurred",
|
||||
logger.String("error_code", appErr.Code()),
|
||||
logger.String("category", appErr.Category()),
|
||||
logger.Int("http_status", appErr.HTTPStatus()),
|
||||
logger.Any("metadata", appErr.Metadata()),
|
||||
logger.String("method", c.Request.Method),
|
||||
logger.String("path", c.Request.URL.Path),
|
||||
logger.String("request_id", c.GetString("request_id")),
|
||||
)
|
||||
}
|
||||
|
||||
// Create error response
|
||||
response := createErrorResponse(c, appErr, cfg)
|
||||
|
||||
// Send response
|
||||
c.JSON(appErr.HTTPStatus(), response)
|
||||
}
|
||||
|
||||
// createErrorResponse creates error response
|
||||
func createErrorResponse(c *gin.Context, err Error, cfg *ErrorMiddlewareConfig) gin.H {
|
||||
lang := c.GetString("language")
|
||||
requestID := c.GetString("request_id")
|
||||
|
||||
response := gin.H{
|
||||
"error": gin.H{
|
||||
"code": err.Code(),
|
||||
"message": err.GetLocalizedMessage(lang),
|
||||
"details": err.Metadata(),
|
||||
"request_id": requestID,
|
||||
"timestamp": time.Now().UTC().Format(time.RFC3339),
|
||||
"retryable": IsRetryable(err.Code()),
|
||||
},
|
||||
"meta": gin.H{
|
||||
"http_status": err.HTTPStatus(),
|
||||
"category": err.Category(),
|
||||
},
|
||||
}
|
||||
|
||||
// Add stack trace in debug mode
|
||||
if gin.Mode() == gin.DebugMode {
|
||||
response["error"].(gin.H)["stack_trace"] = err.Metadata()["stack_trace"]
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
// CustomErrorHandler creates custom error handler
|
||||
func CustomErrorHandler(code string, handler ErrorHandlerFunc) ErrorHandlerFunc {
|
||||
return func(c *gin.Context, err error) {
|
||||
appErr := FromError(err)
|
||||
if appErr != nil && appErr.Code() == code {
|
||||
handler(c, appErr)
|
||||
return
|
||||
}
|
||||
// Fallback to default handling
|
||||
HandleHTTPError(c, err)
|
||||
}
|
||||
}
|
||||
|
||||
// ValidationHandler creates validation error handler
|
||||
func ValidationHandler(c *gin.Context, err error) {
|
||||
appErr := FromError(err)
|
||||
if appErr == nil {
|
||||
appErr = NewWithCode(ErrCodeValidationFailed, err.Error())
|
||||
}
|
||||
|
||||
// Enhance validation errors with field details
|
||||
if appErr.Category() == CategoryValidation {
|
||||
if details, ok := appErr.Metadata()["validation_errors"]; ok {
|
||||
response := gin.H{
|
||||
"error": gin.H{
|
||||
"code": appErr.Code(),
|
||||
"message": appErr.GetLocalizedMessage(getLanguageFromContext(c)),
|
||||
"fields": details,
|
||||
},
|
||||
}
|
||||
c.JSON(http.StatusBadRequest, response)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
HandleHTTPError(c, appErr)
|
||||
}
|
||||
|
||||
// TimeoutHandler creates timeout error handler
|
||||
func TimeoutHandler(c *gin.Context, err error) {
|
||||
appErr := NewWithCode(ErrCodeTimeout, "Request timeout").
|
||||
WithMetadata("timeout", c.GetHeader("X-Timeout")).
|
||||
WithMetadata("endpoint", c.Request.URL.Path)
|
||||
|
||||
HandleHTTPError(c, appErr)
|
||||
}
|
||||
|
||||
// RateLimitHandler creates rate limit error handler
|
||||
func RateLimitHandler(c *gin.Context, err error) {
|
||||
appErr := NewWithCode(ErrCodeRateLimitExceeded, "Rate limit exceeded").
|
||||
WithMetadata("limit", c.GetHeader("X-Rate-Limit-Limit")).
|
||||
WithMetadata("remaining", c.GetHeader("X-Rate-Limit-Remaining")).
|
||||
WithMetadata("reset", c.GetHeader("X-Rate-Limit-Reset"))
|
||||
|
||||
HandleHTTPError(c, appErr)
|
||||
}
|
||||
|
||||
// NotFoundHandler creates 404 error handler
|
||||
func NotFoundHandler(c *gin.Context) {
|
||||
err := NewWithCode(ErrCodeNotFound,
|
||||
fmt.Sprintf("Route %s %s not found", c.Request.Method, c.Request.URL.Path)).
|
||||
WithMetadata("method", c.Request.Method).
|
||||
WithMetadata("path", c.Request.URL.Path).
|
||||
WithMetadata("query", c.Request.URL.RawQuery)
|
||||
|
||||
HandleHTTPError(c, err)
|
||||
}
|
||||
|
||||
// MethodNotAllowedHandler creates 405 error handler
|
||||
func MethodNotAllowedHandler(c *gin.Context) {
|
||||
err := NewWithCode(ErrCodeInvalidInput,
|
||||
fmt.Sprintf("Method %s not allowed for route %s", c.Request.Method, c.Request.URL.Path)).
|
||||
WithMetadata("method", c.Request.Method).
|
||||
WithMetadata("path", c.Request.URL.Path).
|
||||
WithMetadata("allowed_methods", c.GetHeader("Allow"))
|
||||
|
||||
HandleHTTPError(c, err)
|
||||
}
|
||||
|
||||
// GRPCErrorInterceptor creates gRPC error interceptor
|
||||
func GRPCErrorInterceptor(config ...*ErrorMiddlewareConfig) grpc.UnaryServerInterceptor {
|
||||
cfg := DefaultMiddlewareConfig()
|
||||
if len(config) > 0 {
|
||||
cfg = config[0]
|
||||
}
|
||||
|
||||
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
|
||||
resp, err := handler(ctx, req)
|
||||
if err != nil {
|
||||
return nil, handleGRPCError(err, cfg)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
}
|
||||
|
||||
// handleGRPCError handles gRPC error with configuration
|
||||
func handleGRPCError(err error, cfg *ErrorMiddlewareConfig) error {
|
||||
appErr := FromError(err)
|
||||
if appErr == nil {
|
||||
appErr = NewWithCode(ErrCodeInternalError, "Internal server error")
|
||||
}
|
||||
|
||||
// Apply registered handlers
|
||||
appErr = ApplyHandlers(appErr)
|
||||
|
||||
// Sanitize error if needed
|
||||
if cfg.SanitizeErrors {
|
||||
appErr = sanitizeError(appErr).(Error)
|
||||
}
|
||||
|
||||
// Record metrics
|
||||
if cfg.EnableMetrics {
|
||||
RecordError(appErr)
|
||||
}
|
||||
|
||||
// Log error
|
||||
if cfg.LogAllErrors {
|
||||
log := logger.Default()
|
||||
log.Error("gRPC error occurred",
|
||||
logger.String("error_code", appErr.Code()),
|
||||
logger.String("category", appErr.Category()),
|
||||
logger.String("grpc_code", appErr.GRPCCode().String()),
|
||||
logger.Any("metadata", appErr.Metadata()),
|
||||
)
|
||||
}
|
||||
|
||||
// Convert to gRPC status
|
||||
return status.Error(appErr.GRPCCode(), appErr.GetLocalizedMessage(cfg.DefaultLanguage))
|
||||
}
|
||||
|
||||
// GetErrorMetrics returns current error metrics
|
||||
func GetErrorMetrics() ErrorMetrics {
|
||||
m := GetMetrics()
|
||||
return ErrorMetrics{
|
||||
TotalErrors: m.TotalErrors,
|
||||
ErrorsByCode: m.ErrorsByCode,
|
||||
ErrorsByCategory: m.ErrorsByCategory,
|
||||
}
|
||||
}
|
||||
|
||||
// MetricsHandler creates metrics endpoint handler
|
||||
func MetricsHandler() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
metrics := GetErrorMetrics()
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"error_metrics": metrics,
|
||||
"timestamp": time.Now().UTC().Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// HealthHandler creates health endpoint handler
|
||||
func HealthHandler() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "healthy",
|
||||
"timestamp": time.Now().UTC().Format(time.RFC3339),
|
||||
"service": "error-handler",
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,383 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"math/rand"
|
||||
"service/pkg/logger"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// RetryConfig configures retry behavior
|
||||
type RetryConfig struct {
|
||||
MaxAttempts int
|
||||
Delay time.Duration
|
||||
MaxDelay time.Duration
|
||||
Backoff BackoffStrategy
|
||||
RetryIf func(Error) bool
|
||||
OnRetry func(attempt int, err Error)
|
||||
}
|
||||
|
||||
// BackoffStrategy defines backoff strategy
|
||||
type BackoffStrategy int
|
||||
|
||||
const (
|
||||
LinearBackoff BackoffStrategy = iota
|
||||
ExponentialBackoff
|
||||
ExponentialBackoffWithJitter
|
||||
FixedBackoff
|
||||
)
|
||||
|
||||
// DefaultRetryConfig returns default retry configuration
|
||||
func DefaultRetryConfig() *RetryConfig {
|
||||
return &RetryConfig{
|
||||
MaxAttempts: 3,
|
||||
Delay: time.Second,
|
||||
MaxDelay: 30 * time.Second,
|
||||
Backoff: ExponentialBackoff,
|
||||
RetryIf: DefaultRetryCondition,
|
||||
OnRetry: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultRetryCondition determines if error is retryable
|
||||
func DefaultRetryCondition(err Error) bool {
|
||||
return IsRetryable(err.Code())
|
||||
}
|
||||
|
||||
// Retry executes function with retry logic
|
||||
func Retry(fn func() (interface{}, error), config *RetryConfig) (interface{}, error) {
|
||||
if config == nil {
|
||||
config = DefaultRetryConfig()
|
||||
}
|
||||
|
||||
var lastErr Error
|
||||
log := logger.Default()
|
||||
|
||||
for attempt := 0; attempt < config.MaxAttempts; attempt++ {
|
||||
result, err := fn()
|
||||
if err == nil {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
appErr := FromError(err)
|
||||
if appErr == nil {
|
||||
appErr = NewWithCode(ErrCodeInternalError, err.Error())
|
||||
}
|
||||
|
||||
// Check if error is retryable
|
||||
if config.RetryIf != nil && !config.RetryIf(appErr) {
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
lastErr = appErr
|
||||
|
||||
// Don't wait after last attempt
|
||||
if attempt < config.MaxAttempts-1 {
|
||||
delay := calculateDelay(attempt, config)
|
||||
|
||||
if config.OnRetry != nil {
|
||||
config.OnRetry(attempt+1, appErr)
|
||||
}
|
||||
|
||||
log.Warn(appErr.Error(),
|
||||
logger.Int("attempt", attempt+1),
|
||||
logger.Int("max_attempts", config.MaxAttempts),
|
||||
logger.String("retry_delay", delay.String()),
|
||||
logger.Bool("will_retry", attempt < config.MaxAttempts-1),
|
||||
)
|
||||
|
||||
time.Sleep(delay)
|
||||
}
|
||||
}
|
||||
|
||||
return nil, lastErr
|
||||
}
|
||||
|
||||
// RetryWithContext executes function with retry logic and context
|
||||
func RetryWithContext(ctx context.Context, fn func() (interface{}, error), config *RetryConfig) (interface{}, error) {
|
||||
if config == nil {
|
||||
config = DefaultRetryConfig()
|
||||
}
|
||||
|
||||
var lastErr Error
|
||||
log := logger.Default()
|
||||
|
||||
for attempt := 0; attempt < config.MaxAttempts; attempt++ {
|
||||
// Check context cancellation
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, Wrap(ctx.Err(), ErrCodeTimeout, "Context cancelled during retry")
|
||||
default:
|
||||
}
|
||||
|
||||
result, err := fn()
|
||||
if err == nil {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
appErr := FromError(err)
|
||||
if appErr == nil {
|
||||
appErr = NewWithCode(ErrCodeInternalError, err.Error())
|
||||
}
|
||||
|
||||
// Check if error is retryable
|
||||
if config.RetryIf != nil && !config.RetryIf(appErr) {
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
lastErr = appErr
|
||||
|
||||
// Don't wait after last attempt
|
||||
if attempt < config.MaxAttempts-1 {
|
||||
delay := calculateDelay(attempt, config)
|
||||
|
||||
if config.OnRetry != nil {
|
||||
config.OnRetry(attempt+1, appErr)
|
||||
}
|
||||
|
||||
log.Warn(appErr.Error(),
|
||||
logger.Int("attempt", attempt+1),
|
||||
logger.Int("max_attempts", config.MaxAttempts),
|
||||
logger.String("retry_delay", delay.String()),
|
||||
logger.Bool("will_retry", attempt < config.MaxAttempts-1),
|
||||
)
|
||||
|
||||
// Wait with context cancellation
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, Wrap(ctx.Err(), ErrCodeTimeout, "Context cancelled during retry delay")
|
||||
case <-time.After(delay):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, lastErr
|
||||
}
|
||||
|
||||
// calculateDelay calculates delay based on backoff strategy
|
||||
func calculateDelay(attempt int, config *RetryConfig) time.Duration {
|
||||
var delay time.Duration
|
||||
|
||||
switch config.Backoff {
|
||||
case LinearBackoff:
|
||||
delay = time.Duration(attempt+1) * config.Delay
|
||||
case ExponentialBackoff:
|
||||
delay = config.Delay * time.Duration(math.Pow(2, float64(attempt)))
|
||||
case ExponentialBackoffWithJitter:
|
||||
delay = config.Delay * time.Duration(math.Pow(2, float64(attempt)))
|
||||
// Add jitter
|
||||
jitter := time.Duration(rand.Float64() * float64(delay) * 0.1)
|
||||
delay += jitter
|
||||
case FixedBackoff:
|
||||
delay = config.Delay
|
||||
default:
|
||||
delay = config.Delay
|
||||
}
|
||||
|
||||
// Apply max delay limit
|
||||
if delay > config.MaxDelay {
|
||||
delay = config.MaxDelay
|
||||
}
|
||||
|
||||
return delay
|
||||
}
|
||||
|
||||
// CircuitBreaker implements circuit breaker pattern
|
||||
type CircuitBreaker struct {
|
||||
name string
|
||||
maxFailures int
|
||||
resetTimeout time.Duration
|
||||
state CircuitState
|
||||
failures int
|
||||
lastFailTime time.Time
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// CircuitState represents circuit breaker state
|
||||
type CircuitState int
|
||||
|
||||
const (
|
||||
CircuitClosed CircuitState = iota
|
||||
CircuitOpen
|
||||
CircuitHalfOpen
|
||||
)
|
||||
|
||||
// NewCircuitBreaker creates new circuit breaker
|
||||
func NewCircuitBreaker(name string, maxFailures int, resetTimeout time.Duration) *CircuitBreaker {
|
||||
return &CircuitBreaker{
|
||||
name: name,
|
||||
maxFailures: maxFailures,
|
||||
resetTimeout: resetTimeout,
|
||||
state: CircuitClosed,
|
||||
}
|
||||
}
|
||||
|
||||
// Execute executes function with circuit breaker protection
|
||||
func (cb *CircuitBreaker) Execute(fn func() (interface{}, error)) (interface{}, error) {
|
||||
cb.mu.Lock()
|
||||
defer cb.mu.Unlock()
|
||||
|
||||
log := logger.Default()
|
||||
|
||||
// Check if circuit is open
|
||||
if cb.state == CircuitOpen {
|
||||
if time.Since(cb.lastFailTime) > cb.resetTimeout {
|
||||
cb.state = CircuitHalfOpen
|
||||
log.Info("Circuit breaker transitioning to half-open", logger.String("circuit", cb.name))
|
||||
} else {
|
||||
return nil, NewWithCode(ErrCodeServiceUnavailable,
|
||||
fmt.Sprintf("Circuit breaker '%s' is open", cb.name))
|
||||
}
|
||||
}
|
||||
|
||||
// Execute function
|
||||
result, err := fn()
|
||||
if err != nil {
|
||||
cb.onFailure()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cb.onSuccess()
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// onSuccess handles successful execution
|
||||
func (cb *CircuitBreaker) onSuccess() {
|
||||
cb.failures = 0
|
||||
if cb.state == CircuitHalfOpen {
|
||||
cb.state = CircuitClosed
|
||||
log := logger.Default()
|
||||
log.Info("Circuit breaker closed", logger.String("circuit", cb.name))
|
||||
}
|
||||
}
|
||||
|
||||
// onFailure handles failed execution
|
||||
func (cb *CircuitBreaker) onFailure() {
|
||||
cb.failures++
|
||||
cb.lastFailTime = time.Now()
|
||||
|
||||
if cb.failures >= cb.maxFailures {
|
||||
cb.state = CircuitOpen
|
||||
log := logger.Default()
|
||||
log.Warn("Circuit breaker opened",
|
||||
logger.String("circuit", cb.name),
|
||||
logger.Int("failures", cb.failures),
|
||||
logger.Int("max_failures", cb.maxFailures),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// GetState returns current circuit breaker state
|
||||
func (cb *CircuitBreaker) GetState() CircuitState {
|
||||
cb.mu.RLock()
|
||||
defer cb.mu.RUnlock()
|
||||
return cb.state
|
||||
}
|
||||
|
||||
// GetFailures returns current failure count
|
||||
func (cb *CircuitBreaker) GetFailures() int {
|
||||
cb.mu.RLock()
|
||||
defer cb.mu.RUnlock()
|
||||
return cb.failures
|
||||
}
|
||||
|
||||
// Reset resets circuit breaker
|
||||
func (cb *CircuitBreaker) Reset() {
|
||||
cb.mu.Lock()
|
||||
defer cb.mu.Unlock()
|
||||
|
||||
cb.state = CircuitClosed
|
||||
cb.failures = 0
|
||||
cb.lastFailTime = time.Time{}
|
||||
|
||||
log := logger.Default()
|
||||
log.Info("Circuit breaker reset", logger.String("circuit", cb.name))
|
||||
}
|
||||
|
||||
// RecoveryManager manages error recovery strategies
|
||||
type RecoveryManager struct {
|
||||
circuitBreakers map[string]*CircuitBreaker
|
||||
retryConfigs map[string]*RetryConfig
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewRecoveryManager creates new recovery manager
|
||||
func NewRecoveryManager() *RecoveryManager {
|
||||
return &RecoveryManager{
|
||||
circuitBreakers: make(map[string]*CircuitBreaker),
|
||||
retryConfigs: make(map[string]*RetryConfig),
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterCircuitBreaker registers circuit breaker
|
||||
func (rm *RecoveryManager) RegisterCircuitBreaker(name string, maxFailures int, resetTimeout time.Duration) {
|
||||
rm.mu.Lock()
|
||||
defer rm.mu.Unlock()
|
||||
rm.circuitBreakers[name] = NewCircuitBreaker(name, maxFailures, resetTimeout)
|
||||
}
|
||||
|
||||
// RegisterRetryConfig registers retry configuration
|
||||
func (rm *RecoveryManager) RegisterRetryConfig(name string, config *RetryConfig) {
|
||||
rm.mu.Lock()
|
||||
defer rm.mu.Unlock()
|
||||
rm.retryConfigs[name] = config
|
||||
}
|
||||
|
||||
// ExecuteWithRecovery executes function with recovery strategies
|
||||
func (rm *RecoveryManager) ExecuteWithRecovery(operationName string, fn func() (interface{}, error)) (interface{}, error) {
|
||||
rm.mu.RLock()
|
||||
cb, hasCB := rm.circuitBreakers[operationName]
|
||||
config, hasConfig := rm.retryConfigs[operationName]
|
||||
rm.mu.RUnlock()
|
||||
|
||||
// Execute with circuit breaker if available
|
||||
if hasCB {
|
||||
return cb.Execute(func() (interface{}, error) {
|
||||
// Execute with retry if available
|
||||
if hasConfig {
|
||||
return Retry(fn, config)
|
||||
}
|
||||
return fn()
|
||||
})
|
||||
}
|
||||
|
||||
// Execute with retry only
|
||||
if hasConfig {
|
||||
return Retry(fn, config)
|
||||
}
|
||||
|
||||
// Execute normally
|
||||
return fn()
|
||||
}
|
||||
|
||||
// GetCircuitBreaker returns circuit breaker by name
|
||||
func (rm *RecoveryManager) GetCircuitBreaker(name string) (*CircuitBreaker, bool) {
|
||||
rm.mu.RLock()
|
||||
defer rm.mu.RUnlock()
|
||||
cb, exists := rm.circuitBreakers[name]
|
||||
return cb, exists
|
||||
}
|
||||
|
||||
// GetRetryConfig returns retry config by name
|
||||
func (rm *RecoveryManager) GetRetryConfig(name string) (*RetryConfig, bool) {
|
||||
rm.mu.RLock()
|
||||
defer rm.mu.RUnlock()
|
||||
config, exists := rm.retryConfigs[name]
|
||||
return config, exists
|
||||
}
|
||||
|
||||
// Global recovery manager
|
||||
var globalRecoveryManager = NewRecoveryManager()
|
||||
|
||||
// GetRecoveryManager returns global recovery manager
|
||||
func GetRecoveryManager() *RecoveryManager {
|
||||
return globalRecoveryManager
|
||||
}
|
||||
|
||||
// ExecuteWithGlobalRecovery executes with global recovery manager
|
||||
func ExecuteWithGlobalRecovery(operationName string, fn func() (interface{}, error)) (interface{}, error) {
|
||||
return globalRecoveryManager.ExecuteWithRecovery(operationName, fn)
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc/codes"
|
||||
)
|
||||
|
||||
// ErrorType represents a custom error type
|
||||
type ErrorType interface {
|
||||
Name() string
|
||||
New(message string, metadata Metadata) Error
|
||||
FromError(err error) Error
|
||||
Validate(metadata Metadata) error
|
||||
}
|
||||
|
||||
// ErrorRegistry manages custom error types
|
||||
type ErrorRegistry struct {
|
||||
types map[string]ErrorType
|
||||
handlers map[string]func(Error) Error
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
var (
|
||||
registry = &ErrorRegistry{
|
||||
types: make(map[string]ErrorType),
|
||||
handlers: make(map[string]func(Error) Error),
|
||||
}
|
||||
)
|
||||
|
||||
// RegisterErrorType registers a custom error type
|
||||
func RegisterErrorType(name string, errorType ErrorType) {
|
||||
registry.mu.Lock()
|
||||
defer registry.mu.Unlock()
|
||||
registry.types[name] = errorType
|
||||
}
|
||||
|
||||
// GetErrorType returns error type by name
|
||||
func GetErrorType(name string) (ErrorType, bool) {
|
||||
registry.mu.RLock()
|
||||
defer registry.mu.RUnlock()
|
||||
|
||||
errorType, exists := registry.types[name]
|
||||
return errorType, exists
|
||||
}
|
||||
|
||||
// RegisterErrorHandler registers custom error handler
|
||||
func RegisterErrorHandler(code string, handler func(Error) Error) {
|
||||
registry.mu.Lock()
|
||||
defer registry.mu.Unlock()
|
||||
registry.handlers[code] = handler
|
||||
}
|
||||
|
||||
// GetErrorHandler returns error handler for code
|
||||
func GetErrorHandler(code string) (func(Error) Error, bool) {
|
||||
registry.mu.RLock()
|
||||
defer registry.mu.RUnlock()
|
||||
|
||||
handler, exists := registry.handlers[code]
|
||||
return handler, exists
|
||||
}
|
||||
|
||||
// ApplyHandlers applies registered handlers to error
|
||||
func ApplyHandlers(err Error) Error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Apply specific handler for error code
|
||||
if handler, exists := GetErrorHandler(err.Code()); exists {
|
||||
err = handler(err)
|
||||
}
|
||||
|
||||
// Apply category handlers
|
||||
categoryHandlers := GetCategoryHandlers(err.Category())
|
||||
for _, handler := range categoryHandlers {
|
||||
err = handler(err)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// CategoryHandler handles errors by category
|
||||
type CategoryHandler struct {
|
||||
Category string
|
||||
Handler func(Error) Error
|
||||
}
|
||||
|
||||
var (
|
||||
categoryHandlers = make(map[string][]func(Error) Error)
|
||||
categoryMu sync.RWMutex
|
||||
)
|
||||
|
||||
// RegisterCategoryHandler registers handler for error category
|
||||
func RegisterCategoryHandler(category string, handler func(Error) Error) {
|
||||
categoryMu.Lock()
|
||||
defer categoryMu.Unlock()
|
||||
|
||||
if categoryHandlers[category] == nil {
|
||||
categoryHandlers[category] = make([]func(Error) Error, 0)
|
||||
}
|
||||
categoryHandlers[category] = append(categoryHandlers[category], handler)
|
||||
}
|
||||
|
||||
// GetCategoryHandlers returns handlers for category
|
||||
func GetCategoryHandlers(category string) []func(Error) Error {
|
||||
categoryMu.RLock()
|
||||
defer categoryMu.RUnlock()
|
||||
|
||||
handlers := make([]func(Error) Error, len(categoryHandlers[category]))
|
||||
copy(handlers, categoryHandlers[category])
|
||||
return handlers
|
||||
}
|
||||
|
||||
// BaseErrorType provides base implementation for custom error types
|
||||
type BaseErrorType struct {
|
||||
name string
|
||||
defaultCode string
|
||||
category string
|
||||
httpStatus int
|
||||
grpcCode int
|
||||
validator func(Metadata) error
|
||||
}
|
||||
|
||||
// NewBaseErrorType creates new base error type
|
||||
func NewBaseErrorType(name, defaultCode, category string, httpStatus int, grpcCode int) *BaseErrorType {
|
||||
return &BaseErrorType{
|
||||
name: name,
|
||||
defaultCode: defaultCode,
|
||||
category: category,
|
||||
httpStatus: httpStatus,
|
||||
grpcCode: grpcCode,
|
||||
}
|
||||
}
|
||||
|
||||
// Name returns error type name
|
||||
func (bet *BaseErrorType) Name() string {
|
||||
return bet.name
|
||||
}
|
||||
|
||||
// New creates new error of this type
|
||||
func (bet *BaseErrorType) New(message string, metadata Metadata) Error {
|
||||
code := bet.defaultCode
|
||||
if c, exists := metadata["code"].(string); exists {
|
||||
code = c
|
||||
}
|
||||
|
||||
return &AppError{
|
||||
code: code,
|
||||
message: message,
|
||||
category: bet.category,
|
||||
httpStatus: bet.httpStatus,
|
||||
grpcCode: codes.Code(bet.grpcCode),
|
||||
metadata: metadata,
|
||||
timestamp: time.Now(),
|
||||
stackTrace: captureStackTrace(),
|
||||
}
|
||||
}
|
||||
|
||||
// FromError converts existing error to this type
|
||||
func (bet *BaseErrorType) FromError(err error) Error {
|
||||
appErr := FromError(err)
|
||||
if appErr == nil {
|
||||
return bet.New(err.Error(), make(Metadata))
|
||||
}
|
||||
|
||||
return bet.New(appErr.Error(), appErr.Metadata())
|
||||
}
|
||||
|
||||
// Validate validates metadata
|
||||
func (bet *BaseErrorType) Validate(metadata Metadata) error {
|
||||
if bet.validator != nil {
|
||||
return bet.validator(metadata)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetValidator sets validator function
|
||||
func (bet *BaseErrorType) SetValidator(validator func(Metadata) error) {
|
||||
bet.validator = validator
|
||||
}
|
||||
|
||||
// BusinessErrorType represents business logic errors
|
||||
type BusinessErrorType struct {
|
||||
*BaseErrorType
|
||||
ruleName string
|
||||
}
|
||||
|
||||
// NewBusinessErrorType creates new business error type
|
||||
func NewBusinessErrorType(name, ruleName string) *BusinessErrorType {
|
||||
return &BusinessErrorType{
|
||||
BaseErrorType: NewBaseErrorType(name, ErrCodeBusinessRule, CategoryBusiness, 400, int(codes.FailedPrecondition)),
|
||||
ruleName: ruleName,
|
||||
}
|
||||
}
|
||||
|
||||
// RuleName returns business rule name
|
||||
func (bet *BusinessErrorType) RuleName() string {
|
||||
return bet.ruleName
|
||||
}
|
||||
|
||||
// ValidationErrorType represents validation errors
|
||||
type ValidationErrorType struct {
|
||||
*BaseErrorType
|
||||
field string
|
||||
rule string
|
||||
}
|
||||
|
||||
// NewValidationErrorType creates new validation error type
|
||||
func NewValidationErrorType(name, field, rule string) *ValidationErrorType {
|
||||
return &ValidationErrorType{
|
||||
BaseErrorType: NewBaseErrorType(name, ErrCodeValidationFailed, CategoryValidation, 400, int(codes.InvalidArgument)),
|
||||
field: field,
|
||||
rule: rule,
|
||||
}
|
||||
}
|
||||
|
||||
// Field returns field name
|
||||
func (vet *ValidationErrorType) Field() string {
|
||||
return vet.field
|
||||
}
|
||||
|
||||
// Rule returns validation rule
|
||||
func (vet *ValidationErrorType) Rule() string {
|
||||
return vet.rule
|
||||
}
|
||||
|
||||
// ExternalErrorType represents external service errors
|
||||
type ExternalErrorType struct {
|
||||
*BaseErrorType
|
||||
serviceName string
|
||||
retryable bool
|
||||
}
|
||||
|
||||
// NewExternalErrorType creates new external error type
|
||||
func NewExternalErrorType(name, serviceName string, retryable bool) *ExternalErrorType {
|
||||
return &ExternalErrorType{
|
||||
BaseErrorType: NewBaseErrorType(name, ErrCodeExternalError, CategoryExternal, 502, int(codes.Unavailable)),
|
||||
serviceName: serviceName,
|
||||
retryable: retryable,
|
||||
}
|
||||
}
|
||||
|
||||
// ServiceName returns external service name
|
||||
func (eet *ExternalErrorType) ServiceName() string {
|
||||
return eet.serviceName
|
||||
}
|
||||
|
||||
// Retryable returns if error is retryable
|
||||
func (eet *ExternalErrorType) Retryable() bool {
|
||||
return eet.retryable
|
||||
}
|
||||
|
||||
// ErrorFactory creates errors from configuration
|
||||
type ErrorFactory struct {
|
||||
config map[string]interface{}
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewErrorFactory creates new error factory
|
||||
func NewErrorFactory() *ErrorFactory {
|
||||
return &ErrorFactory{
|
||||
config: make(map[string]interface{}),
|
||||
}
|
||||
}
|
||||
|
||||
// Configure configures error factory
|
||||
func (ef *ErrorFactory) Configure(config map[string]interface{}) {
|
||||
ef.mu.Lock()
|
||||
defer ef.mu.Unlock()
|
||||
ef.config = config
|
||||
}
|
||||
|
||||
// CreateError creates error from configuration
|
||||
func (ef *ErrorFactory) CreateError(errorName string, message string, metadata Metadata) Error {
|
||||
ef.mu.RLock()
|
||||
defer ef.mu.RUnlock()
|
||||
|
||||
// Try to get error type from configuration
|
||||
if errorTypeConfig, exists := ef.config[errorName]; exists {
|
||||
if configMap, ok := errorTypeConfig.(map[string]interface{}); ok {
|
||||
name, _ := configMap["name"].(string)
|
||||
if errorType, exists := GetErrorType(name); exists {
|
||||
return errorType.New(message, metadata)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to creating standard error
|
||||
return NewWithMetadata(ErrCodeInternalError, message, metadata)
|
||||
}
|
||||
|
||||
// GetRegisteredTypes returns all registered error types
|
||||
func GetRegisteredTypes() map[string]ErrorType {
|
||||
registry.mu.RLock()
|
||||
defer registry.mu.RUnlock()
|
||||
|
||||
types := make(map[string]ErrorType)
|
||||
for k, v := range registry.types {
|
||||
types[k] = v
|
||||
}
|
||||
return types
|
||||
}
|
||||
|
||||
// GetRegisteredHandlers returns all registered handlers
|
||||
func GetRegisteredHandlers() map[string]func(Error) Error {
|
||||
registry.mu.RLock()
|
||||
defer registry.mu.RUnlock()
|
||||
|
||||
handlers := make(map[string]func(Error) Error)
|
||||
for k, v := range registry.handlers {
|
||||
handlers[k] = v
|
||||
}
|
||||
return handlers
|
||||
}
|
||||
|
||||
// ValidateErrorType validates error type configuration
|
||||
func ValidateErrorType(errorType ErrorType) error {
|
||||
if errorType.Name() == "" {
|
||||
return fmt.Errorf("error type name cannot be empty")
|
||||
}
|
||||
|
||||
// Test error creation
|
||||
testErr := errorType.New("test", make(Metadata))
|
||||
if testErr == nil {
|
||||
return fmt.Errorf("error type failed to create error")
|
||||
}
|
||||
|
||||
if testErr.Code() == "" {
|
||||
return fmt.Errorf("error type must produce error with code")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,429 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ValidationError represents a validation error
|
||||
type ValidationError struct {
|
||||
Field string `json:"field"`
|
||||
Value interface{} `json:"value"`
|
||||
Rule string `json:"rule"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// ValidationErrors represents multiple validation errors
|
||||
type ValidationErrors []ValidationError
|
||||
|
||||
// Error implements error interface
|
||||
func (ve ValidationErrors) Error() string {
|
||||
if len(ve) == 0 {
|
||||
return "validation failed"
|
||||
}
|
||||
|
||||
if len(ve) == 1 {
|
||||
return fmt.Sprintf("validation failed: %s", ve[0].Message)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("validation failed: %d errors", len(ve))
|
||||
}
|
||||
|
||||
// ToMap converts validation errors to map
|
||||
func (ve ValidationErrors) ToMap() map[string]interface{} {
|
||||
result := make(map[string]interface{})
|
||||
for _, err := range ve {
|
||||
result[err.Field] = map[string]interface{}{
|
||||
"value": err.Value,
|
||||
"rule": err.Rule,
|
||||
"message": err.Message,
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Validator provides validation functionality
|
||||
type Validator struct {
|
||||
rules map[string]ValidationRule
|
||||
}
|
||||
|
||||
// ValidationRule represents a validation rule
|
||||
type ValidationRule struct {
|
||||
Name string
|
||||
Validator func(interface{}) error
|
||||
Message string
|
||||
}
|
||||
|
||||
// NewValidator creates new validator
|
||||
func NewValidator() *Validator {
|
||||
return &Validator{
|
||||
rules: make(map[string]ValidationRule),
|
||||
}
|
||||
}
|
||||
|
||||
// AddRule adds validation rule
|
||||
func (v *Validator) AddRule(name string, validator func(interface{}) error, message string) {
|
||||
v.rules[name] = ValidationRule{
|
||||
Name: name,
|
||||
Validator: validator,
|
||||
Message: message,
|
||||
}
|
||||
}
|
||||
|
||||
// Validate validates value against rules
|
||||
func (v *Validator) Validate(value interface{}, rules []string) ValidationErrors {
|
||||
var errors ValidationErrors
|
||||
|
||||
for _, ruleName := range rules {
|
||||
if rule, exists := v.rules[ruleName]; exists {
|
||||
if err := rule.Validator(value); err != nil {
|
||||
errors = append(errors, ValidationError{
|
||||
Field: "",
|
||||
Value: value,
|
||||
Rule: ruleName,
|
||||
Message: rule.Message,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
// ValidateField validates a field with rules
|
||||
func (v *Validator) ValidateField(field string, value interface{}, rules []string) ValidationErrors {
|
||||
var errors ValidationErrors
|
||||
|
||||
for _, ruleName := range rules {
|
||||
if rule, exists := v.rules[ruleName]; exists {
|
||||
if err := rule.Validator(value); err != nil {
|
||||
errors = append(errors, ValidationError{
|
||||
Field: field,
|
||||
Value: value,
|
||||
Rule: ruleName,
|
||||
Message: fmt.Sprintf(rule.Message, field),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
// ValidateStruct validates struct fields
|
||||
func (v *Validator) ValidateStruct(obj interface{}) ValidationErrors {
|
||||
var errors ValidationErrors
|
||||
|
||||
val := reflect.ValueOf(obj)
|
||||
if val.Kind() == reflect.Ptr {
|
||||
val = val.Elem()
|
||||
}
|
||||
|
||||
if val.Kind() != reflect.Struct {
|
||||
return errors
|
||||
}
|
||||
|
||||
typ := val.Type()
|
||||
for i := 0; i < val.NumField(); i++ {
|
||||
field := typ.Field(i)
|
||||
fieldValue := val.Field(i)
|
||||
|
||||
// Get validation tags
|
||||
tag := field.Tag.Get("validate")
|
||||
if tag == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
rules := strings.Split(tag, ",")
|
||||
fieldErrors := v.ValidateField(field.Name, fieldValue.Interface(), rules)
|
||||
errors = append(errors, fieldErrors...)
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
// Common validation rules
|
||||
var (
|
||||
// Required rule
|
||||
Required = ValidationRule{
|
||||
Name: "required",
|
||||
Validator: func(value interface{}) error {
|
||||
if value == nil || value == "" {
|
||||
return fmt.Errorf("value is required")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Message: "%s is required",
|
||||
}
|
||||
|
||||
// Email rule
|
||||
Email = ValidationRule{
|
||||
Name: "email",
|
||||
Validator: func(value interface{}) error {
|
||||
str, ok := value.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("value must be string")
|
||||
}
|
||||
|
||||
emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
|
||||
if !emailRegex.MatchString(str) {
|
||||
return fmt.Errorf("invalid email format")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Message: "%s must be a valid email",
|
||||
}
|
||||
|
||||
// Min length rule
|
||||
MinLength = func(min int) ValidationRule {
|
||||
return ValidationRule{
|
||||
Name: fmt.Sprintf("min_%d", min),
|
||||
Validator: func(value interface{}) error {
|
||||
str, ok := value.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("value must be string")
|
||||
}
|
||||
|
||||
if len(str) < min {
|
||||
return fmt.Errorf("value must be at least %d characters", min)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Message: fmt.Sprintf("%%s must be at least %d characters", min),
|
||||
}
|
||||
}
|
||||
|
||||
// Max length rule
|
||||
MaxLength = func(max int) ValidationRule {
|
||||
return ValidationRule{
|
||||
Name: fmt.Sprintf("max_%d", max),
|
||||
Validator: func(value interface{}) error {
|
||||
str, ok := value.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("value must be string")
|
||||
}
|
||||
|
||||
if len(str) > max {
|
||||
return fmt.Errorf("value must be at most %d characters", max)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Message: fmt.Sprintf("%%s must be at most %d characters", max),
|
||||
}
|
||||
}
|
||||
|
||||
// Numeric rule
|
||||
Numeric = ValidationRule{
|
||||
Name: "numeric",
|
||||
Validator: func(value interface{}) error {
|
||||
switch v := value.(type) {
|
||||
case int, int8, int16, int32, int64:
|
||||
return nil
|
||||
case uint, uint8, uint16, uint32, uint64:
|
||||
return nil
|
||||
case float32, float64:
|
||||
return nil
|
||||
case string:
|
||||
if _, err := strconv.ParseFloat(v, 64); err != nil {
|
||||
return fmt.Errorf("value must be numeric")
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("value must be numeric")
|
||||
}
|
||||
},
|
||||
Message: "%s must be numeric",
|
||||
}
|
||||
|
||||
// Positive rule
|
||||
Positive = ValidationRule{
|
||||
Name: "positive",
|
||||
Validator: func(value interface{}) error {
|
||||
var num float64
|
||||
switch v := value.(type) {
|
||||
case int:
|
||||
num = float64(v)
|
||||
case int64:
|
||||
num = float64(v)
|
||||
case float64:
|
||||
num = v
|
||||
case string:
|
||||
parsed, err := strconv.ParseFloat(v, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("value must be numeric")
|
||||
}
|
||||
num = parsed
|
||||
default:
|
||||
return fmt.Errorf("value must be numeric")
|
||||
}
|
||||
|
||||
if num <= 0 {
|
||||
return fmt.Errorf("value must be positive")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Message: "%s must be positive",
|
||||
}
|
||||
|
||||
// Range rule
|
||||
Range = func(min, max float64) ValidationRule {
|
||||
return ValidationRule{
|
||||
Name: fmt.Sprintf("range_%.1f_%.1f", min, max),
|
||||
Validator: func(value interface{}) error {
|
||||
var num float64
|
||||
switch v := value.(type) {
|
||||
case int:
|
||||
num = float64(v)
|
||||
case int64:
|
||||
num = float64(v)
|
||||
case float64:
|
||||
num = v
|
||||
case string:
|
||||
parsed, err := strconv.ParseFloat(v, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("value must be numeric")
|
||||
}
|
||||
num = parsed
|
||||
default:
|
||||
return fmt.Errorf("value must be numeric")
|
||||
}
|
||||
|
||||
if num < min || num > max {
|
||||
return fmt.Errorf("value must be between %.1f and %.1f", min, max)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Message: fmt.Sprintf("%%s must be between %.1f and %.1f", min, max),
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// DefaultValidator returns validator with common rules
|
||||
func DefaultValidator() *Validator {
|
||||
v := NewValidator()
|
||||
v.AddRule("required", Required.Validator, Required.Message)
|
||||
v.AddRule("email", Email.Validator, Email.Message)
|
||||
v.AddRule("numeric", Numeric.Validator, Numeric.Message)
|
||||
v.AddRule("positive", Positive.Validator, Positive.Message)
|
||||
return v
|
||||
}
|
||||
|
||||
// ValidateStruct validates struct using default validator
|
||||
func ValidateStruct(obj interface{}) ValidationErrors {
|
||||
v := DefaultValidator()
|
||||
return v.ValidateStruct(obj)
|
||||
}
|
||||
|
||||
// CreateValidationError creates validation error
|
||||
func CreateValidationError(field string, value interface{}, rule, message string) Error {
|
||||
metadata := Metadata{
|
||||
"field": field,
|
||||
"value": value,
|
||||
"rule": rule,
|
||||
"validation_errors": map[string]interface{}{
|
||||
field: map[string]interface{}{
|
||||
"value": value,
|
||||
"rule": rule,
|
||||
"message": message,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return NewWithMetadata(ErrCodeValidationFailed,
|
||||
fmt.Sprintf("Validation failed for field %s", field), metadata)
|
||||
}
|
||||
|
||||
// CreateValidationErrors creates error from multiple validation errors
|
||||
func CreateValidationErrors(errors ValidationErrors) Error {
|
||||
if len(errors) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(errors) == 1 {
|
||||
err := errors[0]
|
||||
return CreateValidationError(err.Field, err.Value, err.Rule, err.Message)
|
||||
}
|
||||
|
||||
metadata := Metadata{
|
||||
"validation_errors": errors.ToMap(),
|
||||
"error_count": len(errors),
|
||||
}
|
||||
|
||||
return NewWithMetadata(ErrCodeValidationFailed,
|
||||
fmt.Sprintf("Validation failed with %d errors", len(errors)), metadata)
|
||||
}
|
||||
|
||||
// FieldValidation provides field-level validation
|
||||
type FieldValidation struct {
|
||||
Field string
|
||||
Value interface{}
|
||||
Rules []string
|
||||
}
|
||||
|
||||
// ValidateFields validates multiple fields
|
||||
func ValidateFields(validations []FieldValidation) ValidationErrors {
|
||||
v := DefaultValidator()
|
||||
var allErrors ValidationErrors
|
||||
|
||||
for _, validation := range validations {
|
||||
errors := v.ValidateField(validation.Field, validation.Value, validation.Rules)
|
||||
allErrors = append(allErrors, errors...)
|
||||
}
|
||||
|
||||
return allErrors
|
||||
}
|
||||
|
||||
// ValidationBuilder provides fluent API for validation
|
||||
type ValidationBuilder struct {
|
||||
validations []FieldValidation
|
||||
}
|
||||
|
||||
// NewValidationBuilder creates new validation builder
|
||||
func NewValidationBuilder() *ValidationBuilder {
|
||||
return &ValidationBuilder{
|
||||
validations: make([]FieldValidation, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// Field adds field validation
|
||||
func (vb *ValidationBuilder) Field(field string, value interface{}) *FieldValidationBuilder {
|
||||
return &FieldValidationBuilder{
|
||||
builder: vb,
|
||||
field: field,
|
||||
value: value,
|
||||
}
|
||||
}
|
||||
|
||||
// FieldValidationBuilder builds field validation
|
||||
type FieldValidationBuilder struct {
|
||||
builder *ValidationBuilder
|
||||
field string
|
||||
value interface{}
|
||||
}
|
||||
|
||||
// Rules adds validation rules
|
||||
func (fvb *FieldValidationBuilder) Rules(rules ...string) *ValidationBuilder {
|
||||
fvb.builder.validations = append(fvb.builder.validations, FieldValidation{
|
||||
Field: fvb.field,
|
||||
Value: fvb.value,
|
||||
Rules: rules,
|
||||
})
|
||||
return fvb.builder
|
||||
}
|
||||
|
||||
// Validate executes all validations
|
||||
func (vb *ValidationBuilder) Validate() ValidationErrors {
|
||||
return ValidateFields(vb.validations)
|
||||
}
|
||||
|
||||
// ValidateWithError executes validations and returns error
|
||||
func (vb *ValidationBuilder) ValidateWithError() Error {
|
||||
errors := vb.Validate()
|
||||
if len(errors) > 0 {
|
||||
return CreateValidationErrors(errors)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user