🔀 Merge branch 'feature/json-body' into develop

This commit is contained in:
Dan Jones 2024-01-21 13:51:35 -06:00
commit c18d0e852a
5 changed files with 95 additions and 39 deletions

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.

2
new.go
View file

@ -24,5 +24,5 @@ func NewBadRequest(format string, parts ...any) SettableError {
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

@ -19,8 +19,8 @@ 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().NotNil(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())
} }
@ -28,7 +28,7 @@ func (s *NewTestSuite) TestNotFoundDefaultMsg() {
msg := "Not Found" msg := "Not Found"
var err ResponsableError = NewNotFound("") var err ResponsableError = NewNotFound("")
s.Assert().NotNil(err) s.Assert().NotNil(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())
} }
@ -36,8 +36,8 @@ 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().NotNil(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())
} }
@ -45,7 +45,7 @@ func (s *NewTestSuite) TestBadRequestDefaultMsg() {
msg := "Bad Request" msg := "Bad Request"
var err ResponsableError = NewBadRequest("") var err ResponsableError = NewBadRequest("")
s.Assert().NotNil(err) s.Assert().NotNil(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())
} }
@ -53,5 +53,5 @@ 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().NotNil(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())
} }