Compare commits

...

12 commits

Author SHA1 Message Date
7d4ecc003a 🔀 Merge branch 'rel/0.2.1' into stable 2024-02-07 08:59:34 -06:00
1034ae36da 📝 Update Changelog 2024-02-07 08:59:08 -06:00
0e12c479be Add 401 and 403 default errors 2024-02-07 08:57:14 -06:00
bf9a0a1bfd 🔀 Merge tag 'v0.2.0' into develop
🔖 Add json.Marshaler support
2024-01-21 13:59:53 -06:00
ccdd39a031 🔀 Merge branch 'rel/0.2.0' into stable 2024-01-21 13:58:05 -06:00
7f4cf520a2 📝 Update Changelog and README 2024-01-21 13:56:19 -06:00
c18d0e852a 🔀 Merge branch 'feature/json-body' into develop 2024-01-21 13:51:35 -06:00
2895433239 Add JSON marshalling 2024-01-21 13:50:13 -06:00
f27c669aeb ♻️ Use standard getter/setter names 2024-01-19 22:37:13 -06:00
7fd3192f51 🔀 Merge tag 'v0.1.1' into develop
🔖 Update license
2024-01-17 19:58:23 -06:00
e5ea7dfee8 🔀 Merge branch 'rel/0.1.1' into stable 2024-01-17 19:56:27 -06:00
6ede227022 📝 Update changelog 2024-01-17 19:55:45 -06:00
7 changed files with 146 additions and 45 deletions

View file

@ -1,5 +1,26 @@
# 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
### Changed
- Just the license
## [0.1.0] - 2024-01-17
Initial Release! Hope you like it!

View file

@ -40,4 +40,4 @@ In the second example, `err.GetStatus()` returns 404, `err.GetMsg()` returns "Us
## 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?

View file

@ -1,6 +1,7 @@
package errors
import (
"encoding/json"
"fmt"
"net/http"
)
@ -14,15 +15,16 @@ import (
//
// 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 {
meta := make(map[string]any)
if len(parts) == 0 {
return &erf{status, format, ""}
return &erf{status, format, "", meta}
}
err := fmt.Errorf(format, parts...)
msg := err.Error()
er := &erf{status, msg, ""}
er := &erf{status, msg, "", meta}
if we, ok := err.(wrappedError); ok {
return &wrapErr{er, we.Unwrap()}
}
@ -34,14 +36,16 @@ func Errorf(status int, format string, parts ...any) SettableError {
var _ ResponsableError = new(erf)
var _ SettableError = new(erf)
var _ json.Marshaler = new(erf)
type erf struct {
stat int
err 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 {
e.stat = http.StatusInternalServerError
}
@ -52,15 +56,15 @@ func (e *erf) Error() string {
return e.err
}
func (e *erf) GetMsg() string {
func (e *erf) Msg() string {
if e.msg == "" {
return e.err
}
return e.msg
}
func (e *erf) Status(status int) SettableError {
// GetStatus already handles invalid values, so we'll just ignore this.
func (e *erf) SetStatus(status int) SettableError {
// Status already handles invalid values, so we'll just ignore this.
if status < http.StatusContinue || status >= 600 {
return e
}
@ -68,7 +72,7 @@ func (e *erf) Status(status int) SettableError {
return e
}
func (e *erf) Msg(msg string, parts ...any) SettableError {
func (e *erf) SetMsg(msg string, parts ...any) SettableError {
e.msg = msg
if len(parts) > 0 {
e.msg = fmt.Sprintf(msg, parts...)
@ -76,6 +80,21 @@ func (e *erf) Msg(msg string, parts ...any) SettableError {
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)
type wrapErr struct {

View file

@ -2,6 +2,7 @@ package errors
import (
"errors"
"encoding/json"
"net/http"
"testing"
@ -28,8 +29,8 @@ func (s *ErrorfTestSuite) TestNoWrap() {
exp := "42 is more than 13"
s.Assert().Equal(exp, err.Error())
s.Assert().Equal(exp, err.GetMsg())
s.Assert().Equal(http.StatusTeapot, err.GetStatus())
s.Assert().Equal(exp, err.Msg())
s.Assert().Equal(http.StatusTeapot, err.Status())
}
func (s *ErrorfTestSuite) TestWrapOne() {
@ -45,7 +46,7 @@ func (s *ErrorfTestSuite) TestWrapOne() {
exp := "I'm a little teapot. Here is my handle."
s.Assert().Equal(exp, we.Error())
s.Assert().Equal(exp, we.GetMsg())
s.Assert().Equal(exp, we.Msg())
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."
s.Assert().Equal(exp, we.Error())
s.Assert().Equal(exp, we.GetMsg())
s.Assert().Equal(exp, we.Msg())
unwrapped := we.Unwrap()
s.Assert().Len(unwrapped, 2)
s.Assert().Same(wrappedOne, unwrapped[0])
@ -74,31 +75,52 @@ func (s *ErrorfTestSuite) TestSet() {
var err SettableError = Errorf(http.StatusTeapot, "Unable to BREW")
s.Assert().NotNil(err)
err.Status(http.StatusTooEarly).Msg("It's only %dAM", 2)
s.Assert().Equal(http.StatusTooEarly, err.GetStatus())
s.Assert().Equal("It's only 2AM", err.GetMsg())
err.SetStatus(http.StatusTooEarly).SetMsg("It's only %dAM", 2)
s.Assert().Equal(http.StatusTooEarly, err.Status())
s.Assert().Equal("It's only 2AM", err.Msg())
s.Assert().Equal("Unable to BREW", err.Error())
err.Msg("I am so great")
s.Assert().Equal("I am so great", err.GetMsg())
err.SetMsg("I am so great")
s.Assert().Equal("I am so great", err.Msg())
}
func (s *ErrorfTestSuite) TestGetStatusOutsideRange() {
var err ResponsableError = Errorf(5, "Hello")
s.Assert().NotNil(err)
s.Assert().Equal(http.StatusInternalServerError, err.GetStatus())
s.Assert().Equal(http.StatusInternalServerError, err.Status())
err = Errorf(5, "Hello")
s.Assert().NotNil(err)
s.Assert().Equal(http.StatusInternalServerError, err.GetStatus())
s.Assert().Equal(http.StatusInternalServerError, err.Status())
}
func (s *ErrorfTestSuite) TestSetStatusOutsideRange() {
var err SettableError = Errorf(http.StatusPaymentRequired, "Hello")
s.Assert().NotNil(err)
err.Status(10)
s.Assert().Equal(http.StatusPaymentRequired, err.GetStatus())
err.Status(600)
s.Assert().Equal(http.StatusPaymentRequired, err.GetStatus())
err.SetStatus(10)
s.Assert().Equal(http.StatusPaymentRequired, err.Status())
err.SetStatus(600)
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))
}

View file

@ -1,27 +1,42 @@
package errors
import "encoding/json"
// ResponsableError is an error that has information useful in an HTTP response.
// The string returned by Error should be suitable for logging useful information
// 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.
// 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 {
json.Marshaler
Error() string
GetStatus() int
GetMsg() string
Status() int
Msg() string
JSON() any
}
// 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.
//
// Methods are chainable.
//
// 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 {
ResponsableError
Status(int) SettableError
Msg(string, ...any) SettableError
SetStatus(int) SettableError
SetMsg(string, ...any) SettableError
SetField(string, any) SettableError
}
// UnwrappableError allows a ResponsableError to wrap another error.

12
new.go
View file

@ -20,9 +20,19 @@ func NewBadRequest(format string, parts ...any) SettableError {
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".
func NewInternalError(format string, parts ...any) SettableError {
status := http.StatusInternalServerError
msg := "Unknown Error"
return Errorf(status, format, parts...).Msg(msg)
return Errorf(status, format, parts...).SetMsg(msg)
}

View file

@ -18,40 +18,54 @@ type NewTestSuite struct {
func (s *NewTestSuite) TestNotFound() {
msg := "I can't see you"
var err ResponsableError = NewNotFound(msg)
s.Assert().NotNil(err)
s.Assert().Equal(http.StatusNotFound, err.GetStatus())
s.Assert().Equal(msg, err.GetMsg())
s.Assert().Error(err)
s.Assert().Equal(http.StatusNotFound, err.Status())
s.Assert().Equal(msg, err.Msg())
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() {
msg := "Not Found"
var err ResponsableError = NewNotFound("")
s.Assert().NotNil(err)
s.Assert().Equal(msg, err.GetMsg())
s.Assert().Error(err)
s.Assert().Equal(msg, err.Msg())
s.Assert().Equal(msg, err.Error())
}
func (s *NewTestSuite) TestBadRequest() {
msg := "I can't see you"
var err ResponsableError = NewBadRequest(msg)
s.Assert().NotNil(err)
s.Assert().Equal(http.StatusBadRequest, err.GetStatus())
s.Assert().Equal(msg, err.GetMsg())
s.Assert().Error(err)
s.Assert().Equal(http.StatusBadRequest, err.Status())
s.Assert().Equal(msg, err.Msg())
s.Assert().Equal(msg, err.Error())
}
func (s *NewTestSuite) TestBadRequestDefaultMsg() {
msg := "Bad Request"
var err ResponsableError = NewBadRequest("")
s.Assert().NotNil(err)
s.Assert().Equal(msg, err.GetMsg())
s.Assert().Error(err)
s.Assert().Equal(msg, err.Msg())
s.Assert().Equal(msg, err.Error())
}
func (s *NewTestSuite) TestInternal() {
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("Unknown Error", err.GetMsg())
s.Assert().Equal("Unknown Error", err.Msg())
}