template-monorepo/internal/library/errors/errors.go

294 lines
6.4 KiB
Go
Raw Permalink Normal View History

2026-05-19 11:00:28 +00:00
package errs
import (
"errors"
"fmt"
"net/http"
"gateway/internal/library/errors/code"
)
// Error is a structured application error with an 8-digit code: SSCCCDDD.
type Error struct {
scope code.Scope
category code.Category
detail code.Detail
msg string
internalErr error
}
// New constructs an Error after validating scope, category, and detail.
func New(scope code.Scope, category code.Category, detail code.Detail, displayMsg string) (*Error, error) {
if err := validateComponents(scope, category, detail); err != nil {
return nil, err
}
return &Error{
scope: scope,
category: category,
detail: detail,
msg: displayMsg,
}, nil
}
// MustNew is like New but panics when components are invalid.
// Intended for compile-time constants (Builder helpers).
func MustNew(scope code.Scope, category code.Category, detail code.Detail, displayMsg string) *Error {
e, err := New(scope, category, detail, displayMsg)
if err != nil {
panic(err)
}
return e
}
// For returns a scope-bound builder. Prefer this over package-level globals.
//
// var appErr = errs.For(code.Facade)
// return appErr.ResNotFound("user", id).WithCause(err)
func For(scope code.Scope) Builder {
return Builder{scope: scope}
}
// Error returns the client-facing message.
func (e *Error) Error() string {
if e == nil {
return ""
}
return e.msg
}
func (e *Error) Scope() code.Scope {
if e == nil {
return code.Unset
}
return e.scope
}
func (e *Error) Category() code.Category {
if e == nil {
return code.DefaultCategory
}
return e.category
}
func (e *Error) Detail() code.Detail {
if e == nil {
return code.DefaultDetail
}
return e.detail
}
// SubCode returns the 6-digit CCCDDD portion.
func (e *Error) SubCode() uint32 {
if e == nil {
return code.OK
}
return uint32(e.category)*code.CategoryMultiplier + uint32(e.detail)
}
// Code returns the full 8-digit SSCCCDDD code.
func (e *Error) Code() uint32 {
if e == nil {
return code.NonCode
}
return uint32(e.scope)*code.ScopeMultiplier + e.SubCode()
}
// DisplayCode returns the 8-digit code as a zero-padded string.
func (e *Error) DisplayCode() string {
if e == nil {
return "00000000"
}
return fmt.Sprintf("%08d", e.Code())
}
// Is implements errors.Is semantics using the full 8-digit code.
func (e *Error) Is(target error) bool {
var t *Error
if !errors.As(target, &t) {
return false
}
return e.Code() == t.Code()
}
// Unwrap returns the wrapped cause, if any.
func (e *Error) Unwrap() error {
if e == nil {
return nil
}
return e.internalErr
}
func (e *Error) clone() *Error {
if e == nil {
return nil
}
cp := *e
return &cp
}
// WithCause returns a copy of e with the given cause attached.
func (e *Error) WithCause(cause error) *Error {
if e == nil {
return nil
}
cp := e.clone()
cp.internalErr = cause
return cp
}
// WithScope returns a copy of e using a different scope.
func (e *Error) WithScope(scope code.Scope) (*Error, error) {
if e == nil {
return nil, nil
}
if !scope.Valid() {
return nil, fmt.Errorf("%w: scope %d exceeds max %d", ErrInvalidCode, scope, code.MaxScope)
}
cp := e.clone()
cp.scope = scope
return cp, nil
}
// MustWithScope is like WithScope but panics on invalid scope.
func (e *Error) MustWithScope(scope code.Scope) *Error {
out, err := e.WithScope(scope)
if err != nil {
panic(err)
}
return out
}
// WithDetail returns a copy of e with a different detail code.
func (e *Error) WithDetail(detail code.Detail) (*Error, error) {
if e == nil {
return nil, nil
}
if !detail.Valid() {
return nil, fmt.Errorf("%w: detail %d exceeds max %d", ErrInvalidCode, detail, code.MaxDetail)
}
cp := e.clone()
cp.detail = detail
return cp, nil
}
// WithMessage returns a copy of e with a different client-facing message.
func (e *Error) WithMessage(msg string) *Error {
if e == nil {
return nil
}
cp := e.clone()
cp.msg = msg
return cp
}
// HTTPStatus maps the error category to an HTTP status code.
func (e *Error) HTTPStatus() int {
if e == nil || e.SubCode() == code.OK {
return http.StatusOK
}
switch e.Category() {
case code.InputInvalidFormat, code.InputMissingRequired:
return http.StatusBadRequest
case code.InputNotValidImplementation, code.InputInvalidRange:
return http.StatusUnprocessableEntity
case code.InputUnsupportedMedia:
return http.StatusUnsupportedMediaType
case code.InputPayloadTooLarge:
return http.StatusRequestEntityTooLarge
case code.DBError:
return http.StatusInternalServerError
case code.DBDataConvert:
return http.StatusUnprocessableEntity
case code.DBDuplicate:
return http.StatusConflict
case code.DBUnavailable:
return http.StatusServiceUnavailable
case code.ResNotFound:
return http.StatusNotFound
case code.ResInvalidFormat:
return http.StatusUnprocessableEntity
case code.ResAlreadyExist:
return http.StatusConflict
case code.ResInsufficient, code.ResInvalidMeasureID:
return http.StatusBadRequest
case code.ResInsufficientPerm:
return http.StatusForbidden
case code.ResExpired, code.ResMigrated:
return http.StatusGone
case code.ResInvalidState, code.ResMultiOwner:
return http.StatusConflict
case code.ResInsufficientQuota:
return http.StatusTooManyRequests
case code.ResPreconditionFailed:
return http.StatusPreconditionFailed
case code.ResLocked:
return http.StatusLocked
case code.AuthUnauthorized, code.AuthExpired, code.AuthInvalidPosixTime, code.AuthSigPayloadMismatch:
return http.StatusUnauthorized
case code.AuthForbidden:
return http.StatusForbidden
case code.AuthMethodNotAllowed:
return http.StatusMethodNotAllowed
case code.SysTooManyRequest:
return http.StatusTooManyRequests
case code.SysInternal:
return http.StatusInternalServerError
case code.SysMaintain:
return http.StatusServiceUnavailable
case code.SysTimeout:
return http.StatusGatewayTimeout
case code.SysNotImplemented:
return http.StatusNotImplemented
case code.SysClientTimeout:
return http.StatusRequestTimeout
case code.PSuPublish, code.PSuConsume:
return http.StatusBadGateway
case code.PSuTooLarge:
return http.StatusRequestEntityTooLarge
case code.SvcMaintenance:
return http.StatusServiceUnavailable
case code.SvcInternal:
return http.StatusInternalServerError
case code.SvcThirdParty:
return http.StatusBadGateway
case code.SvcHTTP400:
return http.StatusBadRequest
case code.SvcRateLimited:
return http.StatusTooManyRequests
case code.CatGRPC:
return httpStatusFromGRPCDetail(e.Detail())
}
return http.StatusInternalServerError
}