initial commit
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
// Package app is the service layer. It holds every business operation the
|
||||
// management interface offers, so the HTML handlers and the REST API share one
|
||||
// implementation of validation, auditing and cache invalidation rather than
|
||||
// each growing their own.
|
||||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Error is a user-facing failure with an HTTP status attached.
|
||||
//
|
||||
// Messages are written for an administrator reading them in a toast or a JSON
|
||||
// response: they say what went wrong and, where useful, what to do about it.
|
||||
// Internal detail goes in the wrapped error, which is logged but never shown.
|
||||
type Error struct {
|
||||
Status int
|
||||
Message string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e *Error) Error() string {
|
||||
if e.Err != nil {
|
||||
return fmt.Sprintf("%s: %v", e.Message, e.Err)
|
||||
}
|
||||
return e.Message
|
||||
}
|
||||
|
||||
func (e *Error) Unwrap() error { return e.Err }
|
||||
|
||||
// Invalid reports a client mistake such as a malformed record.
|
||||
func Invalid(format string, args ...any) *Error {
|
||||
return &Error{Status: http.StatusBadRequest, Message: fmt.Sprintf(format, args...)}
|
||||
}
|
||||
|
||||
// NotFound reports a missing object.
|
||||
func NotFound(format string, args ...any) *Error {
|
||||
return &Error{Status: http.StatusNotFound, Message: fmt.Sprintf(format, args...)}
|
||||
}
|
||||
|
||||
// Conflict reports a uniqueness violation.
|
||||
func Conflict(format string, args ...any) *Error {
|
||||
return &Error{Status: http.StatusConflict, Message: fmt.Sprintf(format, args...)}
|
||||
}
|
||||
|
||||
// Forbidden reports an operation the caller may not perform.
|
||||
func Forbidden(format string, args ...any) *Error {
|
||||
return &Error{Status: http.StatusForbidden, Message: fmt.Sprintf(format, args...)}
|
||||
}
|
||||
|
||||
// Internal wraps an unexpected failure. The message is safe to show; err is
|
||||
// logged server-side.
|
||||
func Internal(err error, format string, args ...any) *Error {
|
||||
return &Error{Status: http.StatusInternalServerError, Message: fmt.Sprintf(format, args...), Err: err}
|
||||
}
|
||||
|
||||
// StatusOf maps any error to an HTTP status code.
|
||||
func StatusOf(err error) int {
|
||||
var e *Error
|
||||
if errors.As(err, &e) {
|
||||
return e.Status
|
||||
}
|
||||
return http.StatusInternalServerError
|
||||
}
|
||||
|
||||
// MessageOf returns the user-facing message for an error, falling back to a
|
||||
// generic sentence so internal detail never leaks into a response.
|
||||
func MessageOf(err error) string {
|
||||
var e *Error
|
||||
if errors.As(err, &e) {
|
||||
return e.Message
|
||||
}
|
||||
return "An unexpected error occurred. Check the server log for details."
|
||||
}
|
||||
|
||||
// IsInternal reports whether an error should be logged with its full detail.
|
||||
func IsInternal(err error) bool { return StatusOf(err) >= 500 }
|
||||
Reference in New Issue
Block a user