// Package apperr — доменные ошибки с машинным кодом для i18n. В problem+json их переводит // единственное место — httpx.ResponseError; модули только возвращают *Error. package apperr import ( "fmt" "net/http" ) // FieldError — ошибка конкретного поля запроса. type FieldError struct { Field string `json:"field"` Code string `json:"code"` } // Error — ошибка, которую можно показать клиенту: HTTP-статус, код и параметры для перевода. type Error struct { Status int Code string Detail string Params map[string]any Fields []FieldError // RetryAfter — секунды для заголовка Retry-After (429). RetryAfter int } func (e *Error) Error() string { if e.Detail != "" { return fmt.Sprintf("%s: %s", e.Code, e.Detail) } return e.Code } // Is сравнивает ошибки по коду: errors.Is(err, apperr.Unauthorized("auth.invalid_credentials", "")). func (e *Error) Is(target error) bool { t, ok := target.(*Error) return ok && t.Code == e.Code } // New создаёт ошибку с произвольным статусом. func New(status int, code, detail string) *Error { return &Error{Status: status, Code: code, Detail: detail} } // Unauthorized — 401. func Unauthorized(code, detail string) *Error { return New(http.StatusUnauthorized, code, detail) } // Forbidden — 403. func Forbidden(code, detail string) *Error { return New(http.StatusForbidden, code, detail) } // NotFound — 404. func NotFound(code, detail string) *Error { return New(http.StatusNotFound, code, detail) } // Conflict — 409. func Conflict(code, detail string) *Error { return New(http.StatusConflict, code, detail) } // Validation — 422 с ошибкой одного поля. func Validation(field, code, detail string, params map[string]any) *Error { return &Error{ Status: http.StatusUnprocessableEntity, Code: code, Detail: detail, Params: params, Fields: []FieldError{{Field: field, Code: code}}, } } // TooManyRequests — 429 с Retry-After. func TooManyRequests(code string, retryAfterSec int) *Error { return &Error{Status: http.StatusTooManyRequests, Code: code, Detail: "rate limited", RetryAfter: retryAfterSec} }