80 lines
2.5 KiB
Go
80 lines
2.5 KiB
Go
// 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 }
|