Files

155 lines
5.1 KiB
Go

package domain
import (
"errors"
"fmt"
"strings"
)
// ErrorCode is the stable machine-readable half of an error. Codes never
// change once published; the Chinese message may be reworded freely.
//
// Spec 03 fixes this list. A code is added here only when some caller has to
// branch on it, never to decorate a message.
type ErrorCode string
const (
CodeInvalidArgument ErrorCode = "InvalidArgument"
CodeInvalidConfig ErrorCode = "InvalidConfig"
CodeConflict ErrorCode = "Conflict"
CodeBusy ErrorCode = "Busy"
CodeNotFound ErrorCode = "NotFound"
CodeServiceStopped ErrorCode = "ServiceStopped"
CodeCancelled ErrorCode = "Cancelled"
CodeDeadlineExceeded ErrorCode = "DeadlineExceeded"
CodeBindingUnavailable ErrorCode = "BindingUnavailable"
CodeBindingChanged ErrorCode = "BindingChanged"
CodeDNSFailure ErrorCode = "DNSFailure"
CodeTransportFailure ErrorCode = "TransportFailure"
CodeTLSFailure ErrorCode = "TLSFailure"
CodePortalHTMLResponse ErrorCode = "PortalHTMLResponse"
CodeProtocolInvalid ErrorCode = "ProtocolInvalid"
CodeAuthRejected ErrorCode = "AuthRejected"
CodeOnlineIdentityMismatch ErrorCode = "OnlineIdentityMismatch"
CodeRecoveryRequired ErrorCode = "RecoveryRequired"
CodeUnsupportedCapability ErrorCode = "UnsupportedCapability"
CodeChecksumMismatch ErrorCode = "ChecksumMismatch"
CodePackageIncompatible ErrorCode = "PackageIncompatible"
CodeInstallFailed ErrorCode = "InstallFailed"
// CodeInternal is for a failure this program did not classify: a bug, or an
// error from the standard library that reached the wire unwrapped. Spec 03
// fixes a minimum list and this is above it (D11), because the alternative
// is reporting a fault in the daemon as InvalidArgument -- telling the user
// to correct input that was already correct.
CodeInternal ErrorCode = "Internal"
)
// Error carries one user-facing problem. Field is the dotted path the user can
// act on ("retry.max_seconds", "campus_accounts[1].operator_suffix"); it is
// empty when the problem is not attributable to one field.
//
// Message is written for a user, in Chinese, and must never contain a secret.
// Anything sensitive belongs in the wrapped error, which stays internal.
type Error struct {
Code ErrorCode
Field string
Message string
wrapped error
}
func (e *Error) Error() string {
if e.Field == "" {
return fmt.Sprintf("%s: %s", e.Code, e.Message)
}
return fmt.Sprintf("%s: %s: %s", e.Code, e.Field, e.Message)
}
func (e *Error) Unwrap() error { return e.wrapped }
// Errorf builds an error with no field attribution.
func Errorf(code ErrorCode, format string, args ...any) *Error {
return &Error{Code: code, Message: fmt.Sprintf(format, args...)}
}
// FieldErrorf builds an error the UI can attach to one input.
func FieldErrorf(code ErrorCode, field, format string, args ...any) *Error {
return &Error{Code: code, Field: field, Message: fmt.Sprintf(format, args...)}
}
// Wrap attaches an internal cause. The cause is never rendered to the user, so
// it may hold detail that the message must not.
func (e *Error) Wrap(cause error) *Error {
e.wrapped = cause
return e
}
// CodeOf reports the code of the first *Error in err's chain. Errors that did
// not come from this package are not guessed at: they report false.
func CodeOf(err error) (ErrorCode, bool) {
if typed, ok := errors.AsType[*Error](err); ok {
return typed.Code, true
}
return "", false
}
// Errors is a set of problems reported together.
//
// Config validation returns every problem it found in one pass: a settings form
// that reveals its mistakes one reload at a time is a worse form.
type Errors struct {
Code ErrorCode
Items []*Error
}
func (e *Errors) Error() string {
parts := make([]string, 0, len(e.Items))
for _, item := range e.Items {
parts = append(parts, item.Error())
}
return strings.Join(parts, "; ")
}
// Unwrap exposes the set to errors.Is/As, so a caller that only needs the code
// -- an exit-code mapper, an RPC envelope -- gets the same answer whether one
// problem or ten were reported.
func (e *Errors) Unwrap() []error {
out := make([]error, len(e.Items))
for i, item := range e.Items {
out[i] = item
}
return out
}
// Err returns nil when nothing was collected, so callers can `return v.Err()`.
func (e *Errors) Err() error {
if e == nil || len(e.Items) == 0 {
return nil
}
return e
}
// Add appends one problem, tagging it with the set's code when it has none.
func (e *Errors) Add(item *Error) {
if item.Code == "" {
item.Code = e.Code
}
e.Items = append(e.Items, item)
}
// Addf is the common case: a field problem with the set's code.
func (e *Errors) Addf(field, format string, args ...any) {
e.Add(FieldErrorf(e.Code, field, format, args...))
}
// Fields lists the attributable field paths, in the order they were reported.
func (e *Errors) Fields() []string {
out := make([]string, 0, len(e.Items))
for _, item := range e.Items {
if item.Field != "" {
out = append(out, item.Field)
}
}
return out
}