errors.go 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  1. package models
  2. import (
  3. "errors"
  4. "fmt"
  5. "net/http"
  6. "github.com/gin-gonic/gin"
  7. "github.com/go-playground/validator/v10"
  8. )
  9. // CustomError defines the interface for our application's standard errors.
  10. type CustomError interface {
  11. error
  12. // StatusCode returns the HTTP status code appropriate for this error.
  13. StatusCode() int
  14. // UserMessage returns a user-friendly message for this error.
  15. UserMessage() string
  16. // InternalCode returns a more specific internal error code or message.
  17. InternalCode() string
  18. // Unwrap returns the underlying error, if any.
  19. Unwrap() error
  20. }
  21. // baseError is a base implementation of CustomError.
  22. type baseError struct {
  23. statusCode int
  24. userMessage string
  25. internalCode string
  26. cause error
  27. }
  28. func (e *baseError) Error() string {
  29. if e.cause != nil {
  30. return fmt.Sprintf("%s (internal: %s): %v", e.userMessage, e.internalCode, e.cause)
  31. }
  32. return fmt.Sprintf("%s (internal: %s)", e.userMessage, e.internalCode)
  33. }
  34. func (e *baseError) StatusCode() int {
  35. return e.statusCode
  36. }
  37. func (e *baseError) UserMessage() string {
  38. return e.userMessage
  39. }
  40. func (e *baseError) InternalCode() string {
  41. return e.internalCode
  42. }
  43. func (e *baseError) Unwrap() error {
  44. return e.cause
  45. }
  46. // --- Specific Error Types ---
  47. // ErrNotFound indicates that a requested resource was not found.
  48. type ErrNotFound struct {
  49. baseError
  50. }
  51. func NewErrNotFound(internalCode string, cause error) *ErrNotFound {
  52. return &ErrNotFound{
  53. baseError{
  54. statusCode: http.StatusNotFound,
  55. userMessage: "The requested resource was not found.",
  56. internalCode: internalCode,
  57. cause: cause,
  58. },
  59. }
  60. }
  61. // ErrValidation indicates that input data failed validation.
  62. type ErrValidation struct {
  63. baseError
  64. // ValidationErrors can hold more specific details about which fields failed.
  65. ValidationErrors map[string]string
  66. }
  67. func NewErrValidation(internalCode string, validationErrors map[string]string, cause error) *ErrValidation {
  68. return &ErrValidation{
  69. baseError: baseError{
  70. statusCode: http.StatusBadRequest,
  71. userMessage: "Input validation failed. Please check your data.",
  72. internalCode: internalCode,
  73. cause: cause,
  74. },
  75. ValidationErrors: validationErrors,
  76. }
  77. }
  78. // ErrUnauthorized indicates that the request lacks valid authentication credentials.
  79. type ErrUnauthorized struct {
  80. baseError
  81. }
  82. func NewErrUnauthorized(internalCode string, cause error) *ErrUnauthorized {
  83. return &ErrUnauthorized{
  84. baseError{
  85. statusCode: http.StatusUnauthorized,
  86. userMessage: "Authentication is required and has failed or has not yet been provided.",
  87. internalCode: internalCode,
  88. cause: cause,
  89. },
  90. }
  91. }
  92. // ErrForbidden indicates that the server understood the request but refuses to authorize it.
  93. type ErrForbidden struct {
  94. baseError
  95. }
  96. func NewErrForbidden(internalCode string, cause error) *ErrForbidden {
  97. return &ErrForbidden{
  98. baseError{
  99. statusCode: http.StatusForbidden,
  100. userMessage: "You do not have permission to access this resource.",
  101. internalCode: internalCode,
  102. cause: cause,
  103. },
  104. }
  105. }
  106. // ErrConflict indicates that the request could not be completed due to a conflict with the current state of the resource.
  107. type ErrConflict struct {
  108. baseError
  109. }
  110. func NewErrConflict(internalCode string, cause error) *ErrConflict {
  111. return &ErrConflict{
  112. baseError{
  113. statusCode: http.StatusConflict,
  114. userMessage: "A conflict occurred with the current state of the resource.",
  115. internalCode: internalCode,
  116. cause: cause,
  117. },
  118. }
  119. }
  120. // ErrInternalServer indicates an unexpected condition was encountered on the server.
  121. type ErrInternalServer struct {
  122. baseError
  123. }
  124. func NewErrInternalServer(internalCode string, cause error) *ErrInternalServer {
  125. return &ErrInternalServer{
  126. baseError{
  127. statusCode: http.StatusInternalServerError,
  128. userMessage: "An unexpected error occurred on the server. Please try again later.",
  129. internalCode: internalCode,
  130. cause: cause,
  131. },
  132. }
  133. }
  134. // ErrBadRequest indicates that the server cannot or will not process the request due to something that is perceived to be a client error.
  135. type ErrBadRequest struct {
  136. baseError
  137. }
  138. func NewErrBadRequest(internalCode string, cause error) *ErrBadRequest {
  139. return &ErrBadRequest{
  140. baseError{
  141. statusCode: http.StatusBadRequest,
  142. userMessage: "The request was malformed or invalid.",
  143. internalCode: internalCode,
  144. cause: cause,
  145. },
  146. }
  147. }
  148. // --- Error Predicates ---
  149. // IsErrNotFound checks if an error (or its cause) is an ErrNotFound.
  150. func IsErrNotFound(err error) bool {
  151. var e *ErrNotFound
  152. return errors.As(err, &e)
  153. }
  154. // IsErrValidation checks if an error (or its cause) is an ErrValidation.
  155. func IsErrValidation(err error) bool {
  156. var e *ErrValidation
  157. return errors.As(err, &e)
  158. }
  159. // IsErrUnauthorized checks if an error (or its cause) is an ErrUnauthorized.
  160. func IsErrUnauthorized(err error) bool {
  161. var e *ErrUnauthorized
  162. return errors.As(err, &e)
  163. }
  164. // IsErrForbidden checks if an error (or its cause) is an ErrForbidden.
  165. func IsErrForbidden(err error) bool {
  166. var e *ErrForbidden
  167. return errors.As(err, &e)
  168. }
  169. // IsErrConflict checks if an error (or its cause) is an ErrConflict.
  170. func IsErrConflict(err error) bool {
  171. var e *ErrConflict
  172. return errors.As(err, &e)
  173. }
  174. // IsErrInternalServer checks if an error (or its cause) is an ErrInternalServer.
  175. func IsErrInternalServer(err error) bool {
  176. var e *ErrInternalServer
  177. return errors.As(err, &e)
  178. }
  179. // IsErrForeignKeyViolation is a placeholder for checking foreign key errors.
  180. // This would typically be database-specific.
  181. // For SQLite, you might check for strings like "FOREIGN KEY constraint failed".
  182. // For Postgres, it would be a specific error code.
  183. func IsErrForeignKeyViolation(err error) bool {
  184. if err == nil {
  185. return false
  186. }
  187. // This is a simplistic check. In a real app, you'd use driver-specific error codes/types.
  188. // e.g., for github.com/mattn/go-sqlite3:
  189. // if sqliteErr, ok := err.(sqlite3.Error); ok {
  190. // return sqliteErr.Code == sqlite3.ErrConstraintForeignKey
  191. // }
  192. return false // Placeholder, needs actual DB driver error checking
  193. }
  194. // --- Helper for handlers ---
  195. // RespondWithError checks if the error is a CustomError and sends an appropriate JSON response.
  196. // Otherwise, it sends a generic 500 error.
  197. func RespondWithError(c GinContext, err error) {
  198. var customErr CustomError
  199. if asCustomErr, ok := err.(CustomError); ok { // Check if it directly implements CustomError
  200. customErr = asCustomErr
  201. } else if unwrapErr := AsCustomError(err); unwrapErr != nil { // Check if it wraps a CustomError
  202. customErr = unwrapErr
  203. }
  204. if customErr != nil {
  205. response := gin.H{
  206. "status": "error",
  207. "message": customErr.UserMessage(),
  208. "code": customErr.InternalCode(),
  209. }
  210. // Add validation details for validation errors
  211. if valErr, ok := customErr.(*ErrValidation); ok && valErr.ValidationErrors != nil {
  212. response["details"] = valErr.ValidationErrors
  213. }
  214. c.JSON(customErr.StatusCode(), response)
  215. return
  216. }
  217. // Fallback for non-custom errors (log them as they are unexpected)
  218. // In a real app, you'd log this error with more details.
  219. // log.Printf("Unhandled error: %v", err) // Example logging
  220. c.JSON(http.StatusInternalServerError, gin.H{
  221. "status": "error",
  222. "message": "An unexpected internal server error occurred.",
  223. "code": "INTERNAL_SERVER_ERROR",
  224. })
  225. }
  226. // GinContext is an interface to abstract gin.Context for easier testing or alternative router usage.
  227. type GinContext interface {
  228. JSON(code int, obj interface{})
  229. // Add other gin.Context methods you use in RespondWithError if any
  230. }
  231. // AsCustomError attempts to unwrap err until a CustomError is found or err is nil.
  232. func AsCustomError(err error) CustomError {
  233. for err != nil {
  234. if ce, ok := err.(CustomError); ok {
  235. return ce
  236. }
  237. err = Unwrap(err)
  238. }
  239. return nil
  240. }
  241. // Unwrap is a helper to call Unwrap on an error if the method exists.
  242. func Unwrap(err error) error {
  243. u, ok := err.(interface{ Unwrap() error })
  244. if !ok {
  245. return nil
  246. }
  247. return u.Unwrap()
  248. }
  249. // Helper type for gin.H to avoid direct dependency in models if preferred, though gin.H is just map[string]interface{}
  250. type ginH map[string]interface{}
  251. // ExtractValidationErrors converts validator.ValidationErrors to a map[string]string.
  252. func ExtractValidationErrors(err error) map[string]string {
  253. var ve validator.ValidationErrors
  254. if errors.As(err, &ve) {
  255. out := make(map[string]string)
  256. for _, fe := range ve {
  257. out[fe.Field()] = msgForTag(fe.Tag(), fe.Param())
  258. }
  259. return out
  260. }
  261. return nil
  262. }
  263. // msgForTag returns a user-friendly message for a given validation tag.
  264. func msgForTag(tag string, param string) string {
  265. switch tag {
  266. case "required":
  267. return "This field is required."
  268. case "min":
  269. return fmt.Sprintf("This field must be at least %s characters long.", param)
  270. case "max":
  271. return fmt.Sprintf("This field must be at most %s characters long.", param)
  272. case "email":
  273. return "Invalid email format."
  274. case "oneof":
  275. return fmt.Sprintf("This field must be one of: %s.", param)
  276. // Add more cases for other tags as needed
  277. default:
  278. return "Invalid value."
  279. }
  280. }