63ede5c237
- refactor: move HTTP status and DTO concerns out of model and service layers into API and handler code. - feat: add admin service boundary and route auth through services instead of direct repository access. - test: add architecture and error tests covering package boundaries, redaction, and service behavior. - docs: record the protocol-neutral boundary decision and update architecture and roadmap notes.
90 lines
2.9 KiB
Go
90 lines
2.9 KiB
Go
package model
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
)
|
|
|
|
var (
|
|
ErrNotFound = errors.New("resource not found")
|
|
ErrDuplicate = errors.New("resource already exists")
|
|
ErrUnauthorized = errors.New("unauthorized")
|
|
ErrForbidden = errors.New("forbidden")
|
|
ErrUploadTooLarge = errors.New("upload exceeds maximum size")
|
|
)
|
|
|
|
// ErrorKind classifies domain errors without tying them to a transport.
|
|
type ErrorKind string
|
|
|
|
// Protocol-neutral error kinds.
|
|
const (
|
|
KindInvalidArgument ErrorKind = "invalid_argument"
|
|
KindUnauthenticated ErrorKind = "unauthenticated"
|
|
KindPermissionDenied ErrorKind = "permission_denied"
|
|
KindNotFound ErrorKind = "not_found"
|
|
KindConflict ErrorKind = "conflict"
|
|
KindPayloadTooLarge ErrorKind = "payload_too_large"
|
|
KindInternal ErrorKind = "internal"
|
|
)
|
|
|
|
// AppError is a service-layer error that carries a protocol-neutral kind, a
|
|
// user-safe message, and an optional internal cause for logging.
|
|
type AppError struct {
|
|
Kind ErrorKind
|
|
Message string
|
|
Err error
|
|
}
|
|
|
|
func (e *AppError) Error() string { return e.Message }
|
|
func (e *AppError) Unwrap() error { return e.Err }
|
|
|
|
// NewInvalidArgumentError creates an invalid-argument AppError.
|
|
func NewInvalidArgumentError(message string) *AppError {
|
|
return &AppError{Kind: KindInvalidArgument, Message: message}
|
|
}
|
|
|
|
// NewBadRequestError creates an invalid-argument AppError.
|
|
func NewBadRequestError(message string) *AppError {
|
|
return NewInvalidArgumentError(message)
|
|
}
|
|
|
|
// NewUnauthenticatedError creates an unauthenticated AppError.
|
|
func NewUnauthenticatedError(message string) *AppError {
|
|
return &AppError{Kind: KindUnauthenticated, Message: message}
|
|
}
|
|
|
|
// NewConflictError creates a conflict AppError.
|
|
func NewConflictError(message string) *AppError {
|
|
return &AppError{Kind: KindConflict, Message: message}
|
|
}
|
|
|
|
// NewNotFoundError creates a not-found AppError.
|
|
func NewNotFoundError(message string, cause error) *AppError {
|
|
return &AppError{Kind: KindNotFound, Message: message, Err: cause}
|
|
}
|
|
|
|
// NewPermissionDeniedError creates a permission-denied AppError.
|
|
func NewPermissionDeniedError(message string, cause error) *AppError {
|
|
return &AppError{Kind: KindPermissionDenied, Message: message, Err: cause}
|
|
}
|
|
|
|
// NewForbiddenError creates a permission-denied AppError.
|
|
func NewForbiddenError(message string, cause error) *AppError {
|
|
return NewPermissionDeniedError(message, cause)
|
|
}
|
|
|
|
// NewPayloadTooLargeError creates a payload-too-large AppError.
|
|
func NewPayloadTooLargeError(message string, cause error) *AppError {
|
|
return &AppError{Kind: KindPayloadTooLarge, Message: message, Err: cause}
|
|
}
|
|
|
|
// NewInternalError creates an internal AppError with a wrapped internal cause.
|
|
// msg describes the operation that failed; cause is the underlying error.
|
|
func NewInternalError(msg string, cause error) *AppError {
|
|
return &AppError{
|
|
Kind: KindInternal,
|
|
Message: "internal server error",
|
|
Err: fmt.Errorf("%s: %w", msg, cause),
|
|
}
|
|
}
|