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 }