Compare commits
10 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7d4ecc003a | |||
| 1034ae36da | |||
| 0e12c479be | |||
| bf9a0a1bfd | |||
| ccdd39a031 | |||
| 7f4cf520a2 | |||
| c18d0e852a | |||
| 2895433239 | |||
| f27c669aeb | |||
| 7fd3192f51 |
7 changed files with 140 additions and 45 deletions
15
CHANGELOG.md
15
CHANGELOG.md
|
|
@ -1,5 +1,20 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [0.2.1] - 2024-02-07
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- NewUnauthorized for default 401
|
||||||
|
- NewForbidden for default 403
|
||||||
|
|
||||||
|
## [0.2.0] - 2024-01-21
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- ResponsableError.JSON
|
||||||
|
- ResponsableError extends json.Marshaler
|
||||||
|
- SettableError.SetField
|
||||||
|
|
||||||
## [0.1.1] - 2024-01-17
|
## [0.1.1] - 2024-01-17
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
|
||||||
|
|
@ -40,4 +40,4 @@ In the second example, `err.GetStatus()` returns 404, `err.GetMsg()` returns "Us
|
||||||
|
|
||||||
## Future plans
|
## Future plans
|
||||||
|
|
||||||
I'm thinking about making the errors able to return a default body from `json.Marshal`, possibly with an optional override.`
|
Any suggestions/requests?
|
||||||
|
|
|
||||||
35
errorf.go
35
errorf.go
|
|
@ -1,6 +1,7 @@
|
||||||
package errors
|
package errors
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
@ -14,15 +15,16 @@ import (
|
||||||
//
|
//
|
||||||
// If you want a different user message, use Msg, such as:
|
// If you want a different user message, use Msg, such as:
|
||||||
//
|
//
|
||||||
// err := errors.Errorf(http.StatusNotFound, "%w", sqlError).Msg("user not found %d", userId)
|
// err := errors.Errorf(http.StatusNotFound, "%w", sqlError).SetMsg("user not found %d", userId)
|
||||||
func Errorf(status int, format string, parts ...any) SettableError {
|
func Errorf(status int, format string, parts ...any) SettableError {
|
||||||
|
meta := make(map[string]any)
|
||||||
if len(parts) == 0 {
|
if len(parts) == 0 {
|
||||||
return &erf{status, format, ""}
|
return &erf{status, format, "", meta}
|
||||||
}
|
}
|
||||||
|
|
||||||
err := fmt.Errorf(format, parts...)
|
err := fmt.Errorf(format, parts...)
|
||||||
msg := err.Error()
|
msg := err.Error()
|
||||||
er := &erf{status, msg, ""}
|
er := &erf{status, msg, "", meta}
|
||||||
if we, ok := err.(wrappedError); ok {
|
if we, ok := err.(wrappedError); ok {
|
||||||
return &wrapErr{er, we.Unwrap()}
|
return &wrapErr{er, we.Unwrap()}
|
||||||
}
|
}
|
||||||
|
|
@ -34,14 +36,16 @@ func Errorf(status int, format string, parts ...any) SettableError {
|
||||||
|
|
||||||
var _ ResponsableError = new(erf)
|
var _ ResponsableError = new(erf)
|
||||||
var _ SettableError = new(erf)
|
var _ SettableError = new(erf)
|
||||||
|
var _ json.Marshaler = new(erf)
|
||||||
|
|
||||||
type erf struct {
|
type erf struct {
|
||||||
stat int
|
stat int
|
||||||
err string
|
err string
|
||||||
msg string
|
msg string
|
||||||
|
meta map[string]any
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *erf) GetStatus() int {
|
func (e *erf) Status() int {
|
||||||
if e.stat < http.StatusContinue || e.stat >= 600 {
|
if e.stat < http.StatusContinue || e.stat >= 600 {
|
||||||
e.stat = http.StatusInternalServerError
|
e.stat = http.StatusInternalServerError
|
||||||
}
|
}
|
||||||
|
|
@ -52,15 +56,15 @@ func (e *erf) Error() string {
|
||||||
return e.err
|
return e.err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *erf) GetMsg() string {
|
func (e *erf) Msg() string {
|
||||||
if e.msg == "" {
|
if e.msg == "" {
|
||||||
return e.err
|
return e.err
|
||||||
}
|
}
|
||||||
return e.msg
|
return e.msg
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *erf) Status(status int) SettableError {
|
func (e *erf) SetStatus(status int) SettableError {
|
||||||
// GetStatus already handles invalid values, so we'll just ignore this.
|
// Status already handles invalid values, so we'll just ignore this.
|
||||||
if status < http.StatusContinue || status >= 600 {
|
if status < http.StatusContinue || status >= 600 {
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
@ -68,7 +72,7 @@ func (e *erf) Status(status int) SettableError {
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *erf) Msg(msg string, parts ...any) SettableError {
|
func (e *erf) SetMsg(msg string, parts ...any) SettableError {
|
||||||
e.msg = msg
|
e.msg = msg
|
||||||
if len(parts) > 0 {
|
if len(parts) > 0 {
|
||||||
e.msg = fmt.Sprintf(msg, parts...)
|
e.msg = fmt.Sprintf(msg, parts...)
|
||||||
|
|
@ -76,6 +80,21 @@ func (e *erf) Msg(msg string, parts ...any) SettableError {
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *erf) SetField(field string, value any) SettableError {
|
||||||
|
e.meta[field] = value
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *erf) JSON() any {
|
||||||
|
m := e.meta
|
||||||
|
m["error"] = e.Msg()
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *erf) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(e.JSON())
|
||||||
|
}
|
||||||
|
|
||||||
var _ UnwrappableError = new(wrapErr)
|
var _ UnwrappableError = new(wrapErr)
|
||||||
|
|
||||||
type wrapErr struct {
|
type wrapErr struct {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package errors
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
|
@ -28,8 +29,8 @@ func (s *ErrorfTestSuite) TestNoWrap() {
|
||||||
|
|
||||||
exp := "42 is more than 13"
|
exp := "42 is more than 13"
|
||||||
s.Assert().Equal(exp, err.Error())
|
s.Assert().Equal(exp, err.Error())
|
||||||
s.Assert().Equal(exp, err.GetMsg())
|
s.Assert().Equal(exp, err.Msg())
|
||||||
s.Assert().Equal(http.StatusTeapot, err.GetStatus())
|
s.Assert().Equal(http.StatusTeapot, err.Status())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ErrorfTestSuite) TestWrapOne() {
|
func (s *ErrorfTestSuite) TestWrapOne() {
|
||||||
|
|
@ -45,7 +46,7 @@ func (s *ErrorfTestSuite) TestWrapOne() {
|
||||||
|
|
||||||
exp := "I'm a little teapot. Here is my handle."
|
exp := "I'm a little teapot. Here is my handle."
|
||||||
s.Assert().Equal(exp, we.Error())
|
s.Assert().Equal(exp, we.Error())
|
||||||
s.Assert().Equal(exp, we.GetMsg())
|
s.Assert().Equal(exp, we.Msg())
|
||||||
s.Assert().Same(wrapped, we.Unwrap())
|
s.Assert().Same(wrapped, we.Unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -63,7 +64,7 @@ func (s *ErrorfTestSuite) TestWrapTwo() {
|
||||||
|
|
||||||
exp := "I'm a little teapot: short and stout. Here is my handle."
|
exp := "I'm a little teapot: short and stout. Here is my handle."
|
||||||
s.Assert().Equal(exp, we.Error())
|
s.Assert().Equal(exp, we.Error())
|
||||||
s.Assert().Equal(exp, we.GetMsg())
|
s.Assert().Equal(exp, we.Msg())
|
||||||
unwrapped := we.Unwrap()
|
unwrapped := we.Unwrap()
|
||||||
s.Assert().Len(unwrapped, 2)
|
s.Assert().Len(unwrapped, 2)
|
||||||
s.Assert().Same(wrappedOne, unwrapped[0])
|
s.Assert().Same(wrappedOne, unwrapped[0])
|
||||||
|
|
@ -74,31 +75,52 @@ func (s *ErrorfTestSuite) TestSet() {
|
||||||
var err SettableError = Errorf(http.StatusTeapot, "Unable to BREW")
|
var err SettableError = Errorf(http.StatusTeapot, "Unable to BREW")
|
||||||
s.Assert().NotNil(err)
|
s.Assert().NotNil(err)
|
||||||
|
|
||||||
err.Status(http.StatusTooEarly).Msg("It's only %dAM", 2)
|
err.SetStatus(http.StatusTooEarly).SetMsg("It's only %dAM", 2)
|
||||||
s.Assert().Equal(http.StatusTooEarly, err.GetStatus())
|
s.Assert().Equal(http.StatusTooEarly, err.Status())
|
||||||
s.Assert().Equal("It's only 2AM", err.GetMsg())
|
s.Assert().Equal("It's only 2AM", err.Msg())
|
||||||
s.Assert().Equal("Unable to BREW", err.Error())
|
s.Assert().Equal("Unable to BREW", err.Error())
|
||||||
|
|
||||||
err.Msg("I am so great")
|
err.SetMsg("I am so great")
|
||||||
s.Assert().Equal("I am so great", err.GetMsg())
|
s.Assert().Equal("I am so great", err.Msg())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ErrorfTestSuite) TestGetStatusOutsideRange() {
|
func (s *ErrorfTestSuite) TestGetStatusOutsideRange() {
|
||||||
var err ResponsableError = Errorf(5, "Hello")
|
var err ResponsableError = Errorf(5, "Hello")
|
||||||
s.Assert().NotNil(err)
|
s.Assert().NotNil(err)
|
||||||
s.Assert().Equal(http.StatusInternalServerError, err.GetStatus())
|
s.Assert().Equal(http.StatusInternalServerError, err.Status())
|
||||||
|
|
||||||
err = Errorf(5, "Hello")
|
err = Errorf(5, "Hello")
|
||||||
s.Assert().NotNil(err)
|
s.Assert().NotNil(err)
|
||||||
s.Assert().Equal(http.StatusInternalServerError, err.GetStatus())
|
s.Assert().Equal(http.StatusInternalServerError, err.Status())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ErrorfTestSuite) TestSetStatusOutsideRange() {
|
func (s *ErrorfTestSuite) TestSetStatusOutsideRange() {
|
||||||
var err SettableError = Errorf(http.StatusPaymentRequired, "Hello")
|
var err SettableError = Errorf(http.StatusPaymentRequired, "Hello")
|
||||||
s.Assert().NotNil(err)
|
s.Assert().NotNil(err)
|
||||||
|
|
||||||
err.Status(10)
|
err.SetStatus(10)
|
||||||
s.Assert().Equal(http.StatusPaymentRequired, err.GetStatus())
|
s.Assert().Equal(http.StatusPaymentRequired, err.Status())
|
||||||
err.Status(600)
|
err.SetStatus(600)
|
||||||
s.Assert().Equal(http.StatusPaymentRequired, err.GetStatus())
|
s.Assert().Equal(http.StatusPaymentRequired, err.Status())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ErrorfTestSuite) TestJSON() {
|
||||||
|
var err SettableError = Errorf(http.StatusPaymentRequired, "Hello")
|
||||||
|
s.Assert().NotNil(err)
|
||||||
|
|
||||||
|
m := err.JSON()
|
||||||
|
ma, _ := m.(map[string]any)
|
||||||
|
s.Assert().Equal("Hello", ma["error"])
|
||||||
|
s.Assert().Nil(ma["number"])
|
||||||
|
|
||||||
|
j, _ := json.Marshal(err)
|
||||||
|
s.Assert().Equal(`{"error":"Hello"}`, string(j))
|
||||||
|
|
||||||
|
err.SetField("number",42)
|
||||||
|
m = err.JSON()
|
||||||
|
ma, _ = m.(map[string]any)
|
||||||
|
s.Assert().Equal(42, ma["number"])
|
||||||
|
|
||||||
|
j, _ = json.Marshal(err)
|
||||||
|
s.Assert().Equal(`{"error":"Hello","number":42}`, string(j))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
31
interface.go
31
interface.go
|
|
@ -1,27 +1,42 @@
|
||||||
package errors
|
package errors
|
||||||
|
|
||||||
|
import "encoding/json"
|
||||||
|
|
||||||
// ResponsableError is an error that has information useful in an HTTP response.
|
// ResponsableError is an error that has information useful in an HTTP response.
|
||||||
// The string returned by Error should be suitable for logging useful information
|
// The string returned by Error should be suitable for logging useful information
|
||||||
// to assist debugging the error.
|
// to assist debugging the error.
|
||||||
// GetStatus should return an appropriate HTTP status.
|
//
|
||||||
// GetMsg should return a message suitable to display to the end user. If the message
|
// Status should return an appropriate HTTP status. It must not return < 100 || >= 600.
|
||||||
|
//
|
||||||
|
// Msg should return a message suitable to display to the end user. If the message
|
||||||
// returned by Error is safe for the end user, it may simply call that.
|
// returned by Error is safe for the end user, it may simply call that.
|
||||||
// GetStatus should not return a value outside of the 100 - 599 range.
|
//
|
||||||
|
// JSON should return something that can be marshalled to JSON for the response body.
|
||||||
|
// It can be something as simple as map[string]string{"error":err.Msg()}.
|
||||||
|
// JSON's return value should be used by MarshalJSON()
|
||||||
type ResponsableError interface {
|
type ResponsableError interface {
|
||||||
|
json.Marshaler
|
||||||
Error() string
|
Error() string
|
||||||
GetStatus() int
|
Status() int
|
||||||
GetMsg() string
|
Msg() string
|
||||||
|
JSON() any
|
||||||
}
|
}
|
||||||
|
|
||||||
// SettableError is a ResponsableError which can be modified after initial creation.
|
// SettableError is a ResponsableError which can be modified after initial creation.
|
||||||
// The Status method can set the status, while the Msg method can set user message.
|
//
|
||||||
|
// The SetStatus method can set the status, while the SetMsg method can set user message.
|
||||||
// If any values are passed after the message, it should be passed to fmt.Sprintf for formatting.
|
// If any values are passed after the message, it should be passed to fmt.Sprintf for formatting.
|
||||||
|
//
|
||||||
// Methods are chainable.
|
// Methods are chainable.
|
||||||
|
//
|
||||||
// SetStatus should not use a value outside of 100-599. It may either ignore such values, or panic.
|
// SetStatus should not use a value outside of 100-599. It may either ignore such values, or panic.
|
||||||
|
//
|
||||||
|
// SetField should add some metadata to the error, which will be included in the data returned by JSON()
|
||||||
type SettableError interface {
|
type SettableError interface {
|
||||||
ResponsableError
|
ResponsableError
|
||||||
Status(int) SettableError
|
SetStatus(int) SettableError
|
||||||
Msg(string, ...any) SettableError
|
SetMsg(string, ...any) SettableError
|
||||||
|
SetField(string, any) SettableError
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnwrappableError allows a ResponsableError to wrap another error.
|
// UnwrappableError allows a ResponsableError to wrap another error.
|
||||||
|
|
|
||||||
12
new.go
12
new.go
|
|
@ -20,9 +20,19 @@ func NewBadRequest(format string, parts ...any) SettableError {
|
||||||
return Errorf(http.StatusBadRequest, format, parts...)
|
return Errorf(http.StatusBadRequest, format, parts...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A 401 error with the error message "Unauthorized"
|
||||||
|
func NewUnauthorized() SettableError {
|
||||||
|
return Errorf(http.StatusUnauthorized, "Unauthorized")
|
||||||
|
}
|
||||||
|
|
||||||
|
// A 403 error with the error message "Forbidden"
|
||||||
|
func NewForbidden() SettableError {
|
||||||
|
return Errorf(http.StatusForbidden, "Forbidden")
|
||||||
|
}
|
||||||
|
|
||||||
// Represents a 500 error. For this error, the user error is preset to "Unknown Error".
|
// Represents a 500 error. For this error, the user error is preset to "Unknown Error".
|
||||||
func NewInternalError(format string, parts ...any) SettableError {
|
func NewInternalError(format string, parts ...any) SettableError {
|
||||||
status := http.StatusInternalServerError
|
status := http.StatusInternalServerError
|
||||||
msg := "Unknown Error"
|
msg := "Unknown Error"
|
||||||
return Errorf(status, format, parts...).Msg(msg)
|
return Errorf(status, format, parts...).SetMsg(msg)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
38
new_test.go
38
new_test.go
|
|
@ -18,40 +18,54 @@ type NewTestSuite struct {
|
||||||
func (s *NewTestSuite) TestNotFound() {
|
func (s *NewTestSuite) TestNotFound() {
|
||||||
msg := "I can't see you"
|
msg := "I can't see you"
|
||||||
var err ResponsableError = NewNotFound(msg)
|
var err ResponsableError = NewNotFound(msg)
|
||||||
s.Assert().NotNil(err)
|
s.Assert().Error(err)
|
||||||
s.Assert().Equal(http.StatusNotFound, err.GetStatus())
|
s.Assert().Equal(http.StatusNotFound, err.Status())
|
||||||
s.Assert().Equal(msg, err.GetMsg())
|
s.Assert().Equal(msg, err.Msg())
|
||||||
s.Assert().Equal(msg, err.Error())
|
s.Assert().Equal(msg, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *NewTestSuite) TestUnauthorized() {
|
||||||
|
var err ResponsableError = NewUnauthorized()
|
||||||
|
s.Assert().Error(err)
|
||||||
|
s.Assert().Equal(http.StatusUnauthorized, err.Status())
|
||||||
|
s.Assert().Equal("Unauthorized", err.Msg())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *NewTestSuite) TestForbidden() {
|
||||||
|
var err ResponsableError = NewForbidden()
|
||||||
|
s.Assert().Error(err)
|
||||||
|
s.Assert().Equal(http.StatusForbidden, err.Status())
|
||||||
|
s.Assert().Equal("Forbidden", err.Msg())
|
||||||
|
}
|
||||||
|
|
||||||
func (s *NewTestSuite) TestNotFoundDefaultMsg() {
|
func (s *NewTestSuite) TestNotFoundDefaultMsg() {
|
||||||
msg := "Not Found"
|
msg := "Not Found"
|
||||||
var err ResponsableError = NewNotFound("")
|
var err ResponsableError = NewNotFound("")
|
||||||
s.Assert().NotNil(err)
|
s.Assert().Error(err)
|
||||||
s.Assert().Equal(msg, err.GetMsg())
|
s.Assert().Equal(msg, err.Msg())
|
||||||
s.Assert().Equal(msg, err.Error())
|
s.Assert().Equal(msg, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *NewTestSuite) TestBadRequest() {
|
func (s *NewTestSuite) TestBadRequest() {
|
||||||
msg := "I can't see you"
|
msg := "I can't see you"
|
||||||
var err ResponsableError = NewBadRequest(msg)
|
var err ResponsableError = NewBadRequest(msg)
|
||||||
s.Assert().NotNil(err)
|
s.Assert().Error(err)
|
||||||
s.Assert().Equal(http.StatusBadRequest, err.GetStatus())
|
s.Assert().Equal(http.StatusBadRequest, err.Status())
|
||||||
s.Assert().Equal(msg, err.GetMsg())
|
s.Assert().Equal(msg, err.Msg())
|
||||||
s.Assert().Equal(msg, err.Error())
|
s.Assert().Equal(msg, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *NewTestSuite) TestBadRequestDefaultMsg() {
|
func (s *NewTestSuite) TestBadRequestDefaultMsg() {
|
||||||
msg := "Bad Request"
|
msg := "Bad Request"
|
||||||
var err ResponsableError = NewBadRequest("")
|
var err ResponsableError = NewBadRequest("")
|
||||||
s.Assert().NotNil(err)
|
s.Assert().Error(err)
|
||||||
s.Assert().Equal(msg, err.GetMsg())
|
s.Assert().Equal(msg, err.Msg())
|
||||||
s.Assert().Equal(msg, err.Error())
|
s.Assert().Equal(msg, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *NewTestSuite) TestInternal() {
|
func (s *NewTestSuite) TestInternal() {
|
||||||
var err ResponsableError = NewInternalError("%d > %d", 42, 13)
|
var err ResponsableError = NewInternalError("%d > %d", 42, 13)
|
||||||
s.Assert().NotNil(err)
|
s.Assert().Error(err)
|
||||||
s.Assert().Equal("42 > 13", err.Error())
|
s.Assert().Equal("42 > 13", err.Error())
|
||||||
s.Assert().Equal("Unknown Error", err.GetMsg())
|
s.Assert().Equal("Unknown Error", err.Msg())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue