package errs import ( "errors" "fmt" "net/http" "backend/pkg/library/errors/code" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) // Scope is a global variable that should be set by the service or module. var Scope = code.Unset // Error represents a structured error with an 8-digit code. // The code is composed of a 2-digit scope, a 3-digit category, and a 3-digit detail. // Format: SSCCCDDD type Error struct { scope uint32 // 2-digit service scope category uint32 // 3-digit category detail uint32 // 3-digit detail msg string // Display message for the client internalErr error // The actual underlying error } // New creates a new Error. // It ensures that category is within 0-999 and detail is within 0-999. func New(scope, category, detail uint32, displayMsg string) *Error { if category > uint32(code.MaxCategory) { category = uint32(code.ReservedMaxCategory) } if detail > uint32(code.MaxDetail) { detail = uint32(code.ReservedMaxDetail) } return &Error{ scope: scope, category: category, detail: detail, msg: displayMsg, } } // Error returns the display message. This is intended for the client. // For internal logging and debugging, use Unwrap() to get the underlying error. func (e *Error) Error() string { if e == nil { return "" } return e.msg } // Scope returns the 2-digit scope of the error. func (e *Error) Scope() uint32 { if e == nil { return uint32(code.Unset) } return e.scope } // Category returns the 3-digit category of the error. func (e *Error) Category() uint32 { if e == nil { return uint32(code.DefaultCategory) } return e.category } // Detail returns the 2-digit detail code of the error. func (e *Error) Detail() uint32 { if e == nil { return uint32(code.DefaultDetail) } return e.detail } // SubCode returns the 6-digit code (category + detail). func (e *Error) SubCode() uint32 { if e == nil { return code.OK } c := e.category*code.CategoryMultiplier + e.detail return c } // Code returns the full 8-digit error code (scope + category + detail). func (e *Error) Code() uint32 { if e == nil { return code.NonCode } return e.Scope()*code.ScopeMultiplier + e.SubCode() } // DisplayCode returns the 8-digit error code as a zero-padded string. func (e *Error) DisplayCode() string { if e == nil { return "00000000" } return fmt.Sprintf("%08d", e.Code()) } // Is checks if the target error is of type *Error and has the same sub-code. // It is called by errors.Is(). Do not use it directly. func (e *Error) Is(target error) bool { var err *Error if !errors.As(target, &err) { return false } return e.SubCode() == err.SubCode() } // Unwrap returns the underlying wrapped error. func (e *Error) Unwrap() error { if e == nil { return nil } return e.internalErr } // Wrap sets the internal error for the current error. func (e *Error) Wrap(internalErr error) *Error { if e != nil { e.internalErr = internalErr } return e } // GRPCStatus converts the error to a gRPC status. func (e *Error) GRPCStatus() *status.Status { if e == nil { return status.New(codes.OK, "") } return status.New(codes.Code(e.Code()), e.Error()) } // HTTPStatus returns the corresponding HTTP status code for the error. func (e *Error) HTTPStatus() int { if e == nil || e.SubCode() == code.OK { return http.StatusOK } switch e.Category() { // Input case uint32(code.InputInvalidFormat): return http.StatusBadRequest // 400:輸入格式錯 case uint32(code.InputNotValidImplementation), uint32(code.InputInvalidRange): return http.StatusUnprocessableEntity // 422:語意正確但無法處理(範圍/實作) // DB case uint32(code.DBError): return http.StatusInternalServerError // 500:後端暫時性故障(若你偏好 503 可自行調整) case uint32(code.DBDataConvert): return http.StatusUnprocessableEntity // 422:可修正的資料轉換失敗 case uint32(code.DBDuplicate): return http.StatusConflict // 409:唯一鍵/重複 // Resource case uint32(code.ResNotFound): return http.StatusNotFound // 404:資源不存在 case uint32(code.ResInvalidFormat): return http.StatusUnprocessableEntity // 422:資源表示/格式不符 case uint32(code.ResAlreadyExist): return http.StatusConflict // 409:已存在 case uint32(code.ResInsufficient): return http.StatusBadRequest // 400:數量/容量/條件不足(可由客戶端修正) case uint32(code.ResInsufficientPerm): return http.StatusForbidden // 403:資源層面的權限不足 case uint32(code.ResInvalidMeasureID): return http.StatusBadRequest // 400:ID 無效 case uint32(code.ResExpired): return http.StatusGone // 410:資源已過期/不可用 case uint32(code.ResMigrated): return http.StatusGone // 410:已遷移(若需導引可由上層加 Location) case uint32(code.ResInvalidState): return http.StatusConflict // 409:目前狀態不允許此操作 case uint32(code.ResInsufficientQuota): return http.StatusTooManyRequests // 429:配額不足/達上限 case uint32(code.ResMultiOwner): return http.StatusConflict // 409:多所有者衝突 // Auth case uint32(code.AuthUnauthorized), uint32(code.AuthExpired), uint32(code.AuthInvalidPosixTime), uint32(code.AuthSigPayloadMismatch): return http.StatusUnauthorized // 401:未驗證/無效憑證 case uint32(code.AuthForbidden): return http.StatusForbidden // 403:有身分但沒權限 // System case uint32(code.SysTooManyRequest): return http.StatusTooManyRequests // 429:節流 case uint32(code.SysInternal): return http.StatusInternalServerError // 500:系統內部錯 case uint32(code.SysMaintain): return http.StatusServiceUnavailable // 503:維護中 case uint32(code.SysTimeout): return http.StatusGatewayTimeout // 504:處理/下游逾時 // PubSub case uint32(code.PSuPublish), uint32(code.PSuConsume): return http.StatusBadGateway // 502:訊息中介/外部匯流排失敗 case uint32(code.PSuTooLarge): return http.StatusRequestEntityTooLarge // 413:訊息太大 // Service case uint32(code.SvcMaintenance): return http.StatusServiceUnavailable // 503:服務維護 case uint32(code.SvcInternal): return http.StatusInternalServerError // 500:服務內部錯 case uint32(code.SvcThirdParty): return http.StatusBadGateway // 502:第三方依賴失敗 case uint32(code.SvcHTTP400): return http.StatusBadRequest // 400:明確指派 400 } // fallback return http.StatusInternalServerError }