From f27996b0783c3cf8d52145f5a87664f7d93140d3 Mon Sep 17 00:00:00 2001 From: Ringo Hoffmann Date: Wed, 7 Aug 2024 11:42:15 +0200 Subject: [PATCH] update response model for Error --- error.go | 6 ++-- example_util_test.go | 69 +++++++++++++++++++++++++++++++++-------- examples/server/main.go | 16 +++++----- interfaces.go | 6 ++++ util.go | 61 ++++++++++++++++++------------------ 5 files changed, 104 insertions(+), 54 deletions(-) diff --git a/error.go b/error.go index 86c5385..167ae56 100644 --- a/error.go +++ b/error.go @@ -95,7 +95,7 @@ func Cast(err error, fallback ...ErrorCode) Error { return *lastElkErr } - d, ok := As[Error](err) + d, ok := err.(Error) if !ok { d = Wrap(code, err) d.callStack.offset++ @@ -142,9 +142,9 @@ func WrapCopyCode(err error, message ...string) Error { return e } -// WrapCopyCode wraps the error with a message formatted according to the given +// WrapCopyCodef wraps the error with a message formatted according to the given // format specification keeping the error code of the wrapped error. If the -// wrapped error does not have a error code, CodeUnexpected is set insetad. +// wrapped error does not have a error code, CodeUnexpected is set instead. func WrapCopyCodef(err error, format string, a ...any) Error { e := WrapCopyCode(err, fmt.Sprintf(format, a...)) e.callStack.offset++ diff --git a/example_util_test.go b/example_util_test.go index 2f14e2e..8949c6f 100644 --- a/example_util_test.go +++ b/example_util_test.go @@ -63,38 +63,81 @@ func ExampleIsOfType() { // err: true } +type DetailedError struct { + elk.InnerError + details any +} + +func (t DetailedError) Details() any { + return t.details +} + func ExampleJson() { strErr := errors.New("some error") - dErr := elk.Wrap(elk.ErrorCode("some-error-code"), strErr, "some message") + mErr := elk.Wrap("some-error-code", strErr, "some message") + + json, _ := elk.Json(strErr, 0) + fmt.Println(string(json)) + + json, _ = elk.Json(strErr, 400) + fmt.Println(string(json)) - json, _ := elk.Json(strErr) + json, _ = elk.Json(mErr, 0) fmt.Println(string(json)) - json, _ = elk.Json(strErr, true) + json, _ = elk.Json(mErr, 400) fmt.Println(string(json)) - json, _ = elk.Json(dErr, true) + dtErr := DetailedError{} + dtErr.Inner = elk.NewError("some-error", "an error with details") + dtErr.details = struct { + Foo string + Bar int + }{ + Foo: "foo", + Bar: 123, + } + + json, _ = elk.Json(dtErr, 500) fmt.Println(string(json)) - json, _ = elk.Json(dErr) + dteErr := elk.Wrap("some-detailed-error-wrapped", dtErr, "some detailed error wrapped") + json, _ = elk.Json(dteErr, 500) fmt.Println(string(json)) // Output: // { - // "error": "internal error" + // "Code": "unexpected-error" + // } + // { + // "Code": "unexpected-error", + // "Status": 400 + // } + // { + // "Code": "some-error-code", + // "Message": "some message" // } // { - // "error": "some error" + // "Code": "some-error-code", + // "Message": "some message", + // "Status": 400 // } // { - // "error": "some error", - // "code": "some-error-code", - // "message": "some message" + // "Code": "unexpected-error", + // "Status": 500, + // "Details": { + // "Foo": "foo", + // "Bar": 123 + // } // } // { - // "error": "internal error", - // "code": "some-error-code", - // "message": "some message" + // "Code": "some-detailed-error-wrapped", + // "Message": "some detailed error wrapped", + // "Status": 500, + // "Details": { + // "Foo": "foo", + // "Bar": 123 + // } // } } diff --git a/examples/server/main.go b/examples/server/main.go index 31ad4f0..d194281 100644 --- a/examples/server/main.go +++ b/examples/server/main.go @@ -17,7 +17,7 @@ func main() { mux.HandleFunc("/count", handleCount(ctl)) - http.ListenAndServe(":8080", mux) + _ = http.ListenAndServe(":8080", mux) } func handleCount(ctl *Controller) http.HandlerFunc { @@ -42,19 +42,21 @@ func handleGetCount(ctl *Controller, w http.ResponseWriter, r *http.Request) { res, err := ctl.GetCount(id) if err != nil { + var status int switch elk.Cast(err).Code() { case ErrorCountNotFound: - w.WriteHeader(http.StatusNotFound) + status = http.StatusNotFound default: log.Printf("error: %+.5v\n", err) - w.WriteHeader(http.StatusInternalServerError) + status = http.StatusInternalServerError } - w.Write(elk.MustJson(err)) + w.WriteHeader(status) + _, _ = w.Write(elk.MustJson(err, status)) return } d, _ := json.MarshalIndent(res, "", " ") - w.Write(d) + _, _ = w.Write(d) } func handlePostCount(ctl *Controller, w http.ResponseWriter, r *http.Request) { @@ -71,10 +73,10 @@ func handlePostCount(ctl *Controller, w http.ResponseWriter, r *http.Request) { log.Printf("error: %#.5v\n", err) w.WriteHeader(http.StatusInternalServerError) } - w.Write(elk.MustJson(err)) + _, _ = w.Write(elk.MustJson(err, http.StatusInternalServerError)) return } d, _ := json.MarshalIndent(res, "", " ") - w.Write(d) + _, _ = w.Write(d) } diff --git a/interfaces.go b/interfaces.go index 77c97bf..6fb1c09 100644 --- a/interfaces.go +++ b/interfaces.go @@ -30,6 +30,12 @@ type HasCode interface { Code() ErrorCode } +type HasDetails interface { + error + + Details() any +} + // HasCode describes an error which has a // CallStack. type HasCallStack interface { diff --git a/util.go b/util.go index 5ebdad2..fa71054 100644 --- a/util.go +++ b/util.go @@ -51,11 +51,28 @@ func IsOfType[T error](err error) bool { return false } -type errorJsonModel struct { - Error string `json:"error"` - Code ErrorCode `json:"code,omitempty"` - Message string `json:"message,omitempty"` - Details any `json:"details,omitempty"` +// ErrorResponseModel is used to encode an Error into an API response. +type ErrorResponseModel struct { + Code ErrorCode // The error code + Message string `json:",omitempty"` // An optional short message to further specify the error + Status int `json:",omitempty"` // An optional platform- or protocol-specific status code; i.e. HTTP status code + Details any `json:",omitempty"` // Optional additional detailed context for the error +} + +// ToResponseModel transforms the +func (t Error) ToResponseModel(statusCode int) (model ErrorResponseModel) { + model.Status = statusCode + model.Code = t.Code() + + if mErr, ok := As[HasMessage](t); ok { + model.Message = mErr.Message() + } + + if dErr, ok := As[HasDetails](t); ok { + model.Details = dErr.Details() + } + + return model } // Json takes an error and marshals it into @@ -81,26 +98,8 @@ type errorJsonModel struct { // // When the JSON marshal fails, an error is // returned. -func Json(err error, exposeError ...bool) ([]byte, error) { - var model errorJsonModel - - if len(exposeError) > 0 && exposeError[0] { - if inner := errors.Unwrap(err); inner != nil { - model.Error = inner.Error() - } else { - model.Error = err.Error() - } - } else { - model.Error = "internal error" - } - - if mErr, ok := err.(HasMessage); ok { - model.Message = mErr.Message() - } - - if cErr, ok := err.(HasCode); ok { - model.Code = cErr.Code() - } +func Json(err error, statusCode int) ([]byte, error) { + model := Cast(err).ToResponseModel(statusCode) data, jErr := json.MarshalIndent(model, "", " ") if jErr != nil { @@ -112,14 +111,14 @@ func Json(err error, exposeError ...bool) ([]byte, error) { // MustJson is an alias for Json but panics when // the call to Json returns an error. -func MustJson(err error) []byte { - return mustV(Json(err)) +func MustJson(err error, statusCode int) []byte { + return mustV(Json(err, statusCode)) } // JsonString behaves the same as Json() but returns the result as string instead // of a slice of bytes. -func JsonString(err error, exposeError ...bool) (string, error) { - res, err := Json(err, exposeError...) +func JsonString(err error, statusCode int) (string, error) { + res, err := Json(err, statusCode) if err != nil { return "", err } @@ -127,8 +126,8 @@ func JsonString(err error, exposeError ...bool) (string, error) { } // MustJsonString is an alias for JsonString but panics when the call to Json returns an error. -func MustJsonString(err error) string { - return mustV(JsonString(err)) +func MustJsonString(err error, statusCode int) string { + return mustV(JsonString(err, statusCode)) } func mustV[TV any](v TV, err error) TV {