294 lines
6.4 KiB
Go
294 lines
6.4 KiB
Go
|
|
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
|
||
|
|
}
|