Compare commits

...

10 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
7 changed files with 140 additions and 45 deletions

View file

@ -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

View file

@ -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?

View file

@ -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 {

View file

@ -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))
} }

View file

@ -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
View file

@ -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)
} }

View file

@ -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())
} }