From f27c669aeb7e1f46ec651bac295b4ff3fa91a6e5 Mon Sep 17 00:00:00 2001 From: Dan Jones Date: Fri, 19 Jan 2024 22:37:13 -0600 Subject: [PATCH 1/5] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Use=20standard=20gette?= =?UTF-8?q?r/setter=20names?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- errorf.go | 12 ++++++------ errorf_test.go | 30 +++++++++++++++--------------- interface.go | 20 ++++++++++++-------- new.go | 2 +- new_test.go | 14 +++++++------- 5 files changed, 41 insertions(+), 37 deletions(-) diff --git a/errorf.go b/errorf.go index 03bde47..9be66cc 100644 --- a/errorf.go +++ b/errorf.go @@ -14,7 +14,7 @@ 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 { if len(parts) == 0 { return &erf{status, format, ""} @@ -41,7 +41,7 @@ type erf struct { msg string } -func (e *erf) GetStatus() int { +func (e *erf) Status() int { if e.stat < http.StatusContinue || e.stat >= 600 { e.stat = http.StatusInternalServerError } @@ -52,15 +52,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 +68,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...) diff --git a/errorf_test.go b/errorf_test.go index 1a83ca0..5abc136 100644 --- a/errorf_test.go +++ b/errorf_test.go @@ -28,8 +28,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 +45,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 +63,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 +74,31 @@ 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()) } diff --git a/interface.go b/interface.go index da03a48..38c0ec2 100644 --- a/interface.go +++ b/interface.go @@ -3,25 +3,29 @@ package errors // 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. type ResponsableError interface { Error() string - GetStatus() int - GetMsg() string + Status() int + Msg() string } // 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. type SettableError interface { ResponsableError - Status(int) SettableError - Msg(string, ...any) SettableError + SetStatus(int) SettableError + SetMsg(string, ...any) SettableError } // UnwrappableError allows a ResponsableError to wrap another error. diff --git a/new.go b/new.go index 73e1cf6..0adc51d 100644 --- a/new.go +++ b/new.go @@ -24,5 +24,5 @@ func NewBadRequest(format string, parts ...any) SettableError { 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) } diff --git a/new_test.go b/new_test.go index fc74651..12cae4d 100644 --- a/new_test.go +++ b/new_test.go @@ -19,8 +19,8 @@ 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().Equal(http.StatusNotFound, err.Status()) + s.Assert().Equal(msg, err.Msg()) s.Assert().Equal(msg, err.Error()) } @@ -28,7 +28,7 @@ func (s *NewTestSuite) TestNotFoundDefaultMsg() { msg := "Not Found" var err ResponsableError = NewNotFound("") s.Assert().NotNil(err) - s.Assert().Equal(msg, err.GetMsg()) + s.Assert().Equal(msg, err.Msg()) s.Assert().Equal(msg, err.Error()) } @@ -36,8 +36,8 @@ 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().Equal(http.StatusBadRequest, err.Status()) + s.Assert().Equal(msg, err.Msg()) s.Assert().Equal(msg, err.Error()) } @@ -45,7 +45,7 @@ func (s *NewTestSuite) TestBadRequestDefaultMsg() { msg := "Bad Request" var err ResponsableError = NewBadRequest("") s.Assert().NotNil(err) - s.Assert().Equal(msg, err.GetMsg()) + s.Assert().Equal(msg, err.Msg()) s.Assert().Equal(msg, err.Error()) } @@ -53,5 +53,5 @@ func (s *NewTestSuite) TestInternal() { var err ResponsableError = NewInternalError("%d > %d", 42, 13) s.Assert().NotNil(err) s.Assert().Equal("42 > 13", err.Error()) - s.Assert().Equal("Unknown Error", err.GetMsg()) + s.Assert().Equal("Unknown Error", err.Msg()) } From 2895433239b5c2e583a76abbbdd7e4a32c55dd9f Mon Sep 17 00:00:00 2001 From: Dan Jones Date: Sun, 21 Jan 2024 13:50:13 -0600 Subject: [PATCH 2/5] =?UTF-8?q?=E2=9C=A8=20Add=20JSON=20marshalling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- errorf.go | 23 +++++++++++++++++++++-- errorf_test.go | 22 ++++++++++++++++++++++ interface.go | 11 +++++++++++ 3 files changed, 54 insertions(+), 2 deletions(-) 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. From 7f4cf520a24e90d8540653ed03177bac2a5da19e Mon Sep 17 00:00:00 2001 From: Dan Jones Date: Sun, 21 Jan 2024 13:56:19 -0600 Subject: [PATCH 3/5] =?UTF-8?q?=F0=9F=93=9D=20Update=20Changelog=20and=20R?= =?UTF-8?q?EADME?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 8 ++++++++ README.md | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7272375..8a6f348 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [0.2.0] - 2024-01-21 + +### Added + +- ResponsableError.JSON +- ResponsableError extends json.Marshaler +- SettableError.SetField + ## [0.1.1] - 2024-01-17 ### Changed diff --git a/README.md b/README.md index dc401b7..4df69c0 100644 --- a/README.md +++ b/README.md @@ -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? From 0e12c479be415bac7b1f9b9794ba613a5f51a911 Mon Sep 17 00:00:00 2001 From: Dan Jones Date: Wed, 7 Feb 2024 08:57:14 -0600 Subject: [PATCH 4/5] =?UTF-8?q?=E2=9C=A8=20Add=20401=20and=20403=20default?= =?UTF-8?q?=20errors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- new.go | 10 ++++++++++ new_test.go | 24 +++++++++++++++++++----- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/new.go b/new.go index 0adc51d..a4c7eef 100644 --- a/new.go +++ b/new.go @@ -20,6 +20,16 @@ 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 diff --git a/new_test.go b/new_test.go index 12cae4d..71ef3c8 100644 --- a/new_test.go +++ b/new_test.go @@ -18,16 +18,30 @@ type NewTestSuite struct { func (s *NewTestSuite) TestNotFound() { msg := "I can't see you" var err ResponsableError = NewNotFound(msg) - s.Assert().NotNil(err) + 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().Error(err) s.Assert().Equal(msg, err.Msg()) s.Assert().Equal(msg, err.Error()) } @@ -35,7 +49,7 @@ func (s *NewTestSuite) TestNotFoundDefaultMsg() { func (s *NewTestSuite) TestBadRequest() { msg := "I can't see you" var err ResponsableError = NewBadRequest(msg) - s.Assert().NotNil(err) + s.Assert().Error(err) s.Assert().Equal(http.StatusBadRequest, err.Status()) s.Assert().Equal(msg, err.Msg()) s.Assert().Equal(msg, err.Error()) @@ -44,14 +58,14 @@ func (s *NewTestSuite) TestBadRequest() { func (s *NewTestSuite) TestBadRequestDefaultMsg() { msg := "Bad Request" var err ResponsableError = NewBadRequest("") - s.Assert().NotNil(err) + 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.Msg()) } From 1034ae36dae0d63e9105aec11865920000038d51 Mon Sep 17 00:00:00 2001 From: Dan Jones Date: Wed, 7 Feb 2024 08:59:08 -0600 Subject: [PATCH 5/5] =?UTF-8?q?=F0=9F=93=9D=20Update=20Changelog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a6f348..0f05820 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [0.2.1] - 2024-02-07 + +### Added + +- NewUnauthorized for default 401 +- NewForbidden for default 403 + ## [0.2.0] - 2024-01-21 ### Added