refactor(api): enforce protocol-neutral service boundaries
- 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.
This commit is contained in:
+46
-19
@@ -3,7 +3,6 @@ package model
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -14,48 +13,76 @@ var (
|
||||
ErrUploadTooLarge = errors.New("upload exceeds maximum size")
|
||||
)
|
||||
|
||||
// AppError is a service-layer error that carries an HTTP status, a user-safe
|
||||
// message, and an optional internal cause for logging. Handlers unpack it
|
||||
// transparently via api.RespondError.
|
||||
// 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 {
|
||||
Status int // HTTP status code
|
||||
Message string // safe for API response
|
||||
Err error // internal cause (nil = user-caused, skip logging)
|
||||
Kind ErrorKind
|
||||
Message string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e *AppError) Error() string { return e.Message }
|
||||
func (e *AppError) Unwrap() error { return e.Err }
|
||||
|
||||
// NewBadRequestError creates a 400 AppError.
|
||||
// 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 &AppError{Status: http.StatusBadRequest, Message: message}
|
||||
return NewInvalidArgumentError(message)
|
||||
}
|
||||
|
||||
// NewConflictError creates a 409 AppError.
|
||||
// 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{Status: http.StatusConflict, Message: message}
|
||||
return &AppError{Kind: KindConflict, Message: message}
|
||||
}
|
||||
|
||||
// NewNotFoundError creates a 404 AppError.
|
||||
// NewNotFoundError creates a not-found AppError.
|
||||
func NewNotFoundError(message string, cause error) *AppError {
|
||||
return &AppError{Status: http.StatusNotFound, Message: message, Err: cause}
|
||||
return &AppError{Kind: KindNotFound, Message: message, Err: cause}
|
||||
}
|
||||
|
||||
// NewForbiddenError creates a 403 AppError.
|
||||
// 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 &AppError{Status: http.StatusForbidden, Message: message, Err: cause}
|
||||
return NewPermissionDeniedError(message, cause)
|
||||
}
|
||||
|
||||
// NewPayloadTooLargeError creates a 413 AppError.
|
||||
// NewPayloadTooLargeError creates a payload-too-large AppError.
|
||||
func NewPayloadTooLargeError(message string, cause error) *AppError {
|
||||
return &AppError{Status: http.StatusRequestEntityTooLarge, Message: message, Err: cause}
|
||||
return &AppError{Kind: KindPayloadTooLarge, Message: message, Err: cause}
|
||||
}
|
||||
|
||||
// NewInternalError creates a 500 AppError with a wrapped internal 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{
|
||||
Status: http.StatusInternalServerError,
|
||||
Kind: KindInternal,
|
||||
Message: "internal server error",
|
||||
Err: fmt.Errorf("%s: %w", msg, cause),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user