diff --git a/errorf.go b/errorf.go index 9be66cc..5f371e5 100644 --- a/errorf.go +++ b/errorf.go @@ -1,6 +1,7 @@ package errors import ( + "encoding/json" "fmt" "net/http" ) @@ -16,13 +17,14 @@ import ( // // 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,11 +36,13 @@ 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) Status() int { @@ -76,6 +80,21 @@ func (e *erf) SetMsg(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 { diff --git a/errorf_test.go b/errorf_test.go index 5abc136..3847ca2 100644 --- a/errorf_test.go +++ b/errorf_test.go @@ -2,6 +2,7 @@ package errors import ( "errors" + "encoding/json" "net/http" "testing" @@ -102,3 +103,24 @@ func (s *ErrorfTestSuite) TestSetStatusOutsideRange() { 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)) +} diff --git a/interface.go b/interface.go index 38c0ec2..5701b3b 100644 --- a/interface.go +++ b/interface.go @@ -1,5 +1,7 @@ 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. @@ -8,10 +10,16 @@ package errors // // 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. +// +// 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 Status() int Msg() string + JSON() any } // SettableError is a ResponsableError which can be modified after initial creation. @@ -22,10 +30,13 @@ type ResponsableError interface { // 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 SetStatus(int) SettableError SetMsg(string, ...any) SettableError + SetField(string, any) SettableError } // UnwrappableError allows a ResponsableError to wrap another error.