Skip to content

Commit

Permalink
cleanup some wrapping functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
Greg Weber committed Nov 18, 2024
1 parent f9cbafe commit d7839dd
Show file tree
Hide file tree
Showing 8 changed files with 100 additions and 68 deletions.
10 changes: 5 additions & 5 deletions error_code.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,15 @@
// A few generic top-level error codes are provided (see the variables section of the doc).
// You are encouraged to create your own error codes customized to your application rather than solely using generic errors.
//
// See NewJSONFormat for an opinion on how to send back meta data about errors with the error data to a client.
// JSONFormat includes a body of response data (the "data field") that is by default the data from the Error
// serialized to JSON.
//
// Stack traces are automatically added by NewInternalErr and show up as the Stack field in JSONFormat.
// Errors can be grouped with Combine() and ungrouped via Errors() which show up as the Others field in JSONFormat.
// Error Codes can be grouped with Combine() and ungrouped via ErrorsCodes() which show up as the Others field in JSONFormat.
//
// To extract any ErrorCodes from an error, use CodeChain().
// This extracts error codes without information loss (using ChainContext).
//
// See NewJSONFormat for an opinion on how to send back meta data about errors with the error data to a client.
// JSONFormat includes a body of response data (the "data field") that is by default the data from the Error
// serialized to JSON.
package errcode

import (
Expand Down
11 changes: 6 additions & 5 deletions error_code_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -362,22 +362,23 @@ func TestUserMsg(t *testing.T) {
if errcode.GetUserMsg(umEmpty.AddTo(MinimalError{})) != "" {
t.Errorf("expected empty string")
}
if umEmpty.AddTo(nil) != nil {
t.Errorf("expected nil")
nilAdd := umEmpty.AddTo(nil)
if nilAdd != nil {
t.Errorf("expected nil but got %+v", nilAdd)
}

UserMsgEquals(t, &ErrorWrapper{Err: ue}, "user")
UserMsgEquals(t, &ErrorWrapper{Err: UserMsgErrorEmbed{EmbedUserMsg: errcode.EmbedUserMsg{Msg: "field"}}}, "field")

msgErrCode := errcode.UserMsgErrCode{Msg: "msg", ErrorCode: MinimalError{}}
msgErrCode := errcode.WithUserMsg("msg", MinimalError{})
AssertUserMsg(t, msgErrCode, "msg")
UserMsgEquals(t, msgErrCode, "msg")

UserMsgEquals(t, &ErrorWrapper{Err: msgErrCode}, "msg")
wrappedUser := &ErrorWrapper{Err: errcode.UserMsgErrCode{Msg: "msg", ErrorCode: ue}}
wrappedUser := &ErrorWrapper{Err: errcode.WithUserMsg("msg", ue)}
AssertUserMsg(t, wrappedUser, "msg")
UserMsgEquals(t, wrappedUser, "msg")
UserMsgEquals(t, errcode.UserMsgErrCode{Msg: "msg", ErrorCode: ue}, "msg")
UserMsgEquals(t, errcode.WithUserMsg("msg", ue), "msg")
}

func AssertCodes(t *testing.T, code errcode.ErrorCode, codeStrs ...errcode.CodeStr) {
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ module github.com/gregwebs/errcode

go 1.21.9

require github.com/gregwebs/errors v1.16.0
require github.com/gregwebs/errors v1.17.0
28 changes: 18 additions & 10 deletions group.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import (
"github.com/gregwebs/errors"
)

// ErrorCodes return all errors (from an ErrorGroup) that are of interface ErrorCode.
// ErrorCodes return all errors (including those grouped) that are of interface ErrorCode.
// It first calls the Errors function.
func ErrorCodes(err error) []ErrorCode {
errorCodes := make([]ErrorCode, 0)
Expand Down Expand Up @@ -64,10 +64,10 @@ func combineGeneric[Err ErrorCode](initial Err, others ...error) *multiCode[Err]
}
}

var _ ErrorCode = (*multiCode[ErrorCode])(nil) // assert implements interface
var _ unwrapError = (*multiCode[ErrorCode])(nil) // assert implements interface
var _ errors.ErrorGroup = (*multiCode[ErrorCode])(nil) // assert implements interface
var _ fmt.Formatter = (*multiCode[ErrorCode])(nil) // assert implements interface
var _ ErrorCode = (*multiCode[ErrorCode])(nil) // assert implements interface
var _ unwrapsError = (*multiCode[ErrorCode])(nil) // assert implements interface
var _ errorGroup = (*multiCode[ErrorCode])(nil) // assert implements interface
var _ fmt.Formatter = (*multiCode[ErrorCode])(nil) // assert implements interface

// A MultiErrorCode contains at least one ErrorCode and uses that to satisfy the ErrorCode and related interfaces
// The Error method will produce a string of all the errors with a semi-colon separation.
Expand Down Expand Up @@ -116,7 +116,7 @@ func (e multiCode[Err]) Error() string {
return output
}

// Errors fullfills the ErrorGroup inteface
// Errors fullfills the errorGroup inteface
func (e multiCode[Err]) Errors() []error {
return append([]error{error(e.ErrCode)}, e.rest...)
}
Expand All @@ -127,23 +127,31 @@ func (e multiCode[Err]) Code() Code {
}

// Unwrap fullfills the errors package Unwrap function
func (e multiCode[Err]) Unwrap() error {
return e.ErrCode
func (e multiCode[Err]) Unwrap() []error {
return e.Errors()
}

func (e multiCode[Err]) First() Err {
return e.ErrCode
}

type unwrapsError interface {
Unwrap() []error
}

type errorGroup interface {
Errors() []error
}

// CodeChain resolves wrapped errors down to the first ErrorCode.
// An error that is an ErrorGroup with multiple codes will have its error codes combined to a MultiErrorCode.
// An error that is a grouping with multiple codes will have its error codes combined to a MultiErrorCode.
// If the given error is not an ErrorCode, a ContextChain will be returned with Top set to the given error.
// This allows the return object to maintain a full Error() message.
func CodeChain(errInput error) ErrorCode {
checkError := func(err error) ErrorCode {
if errCode, ok := err.(ErrorCode); ok {
return errCode
} else if eg, ok := err.(errors.ErrorGroup); ok {
} else if eg, ok := err.(errorGroup); ok {
group := []ErrorCode{}
for _, errItem := range eg.Errors() {
if itemCode := CodeChain(errItem); itemCode != nil {
Expand Down
4 changes: 4 additions & 0 deletions group_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ func (e MultiErrors) Errors() []error {
return e.Multi
}

func (e MultiErrors) Unwrap() []error {
return e.Multi
}

var _ error = MultiErrors{}
var _ errors.ErrorGroup = MultiErrors{}

Expand Down
35 changes: 22 additions & 13 deletions user.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,29 +58,33 @@ func (e EmbedUserMsg) GetUserMsg() string {
// UserMsgErrCode is an ErrorCode with a Msg field attached.
// This can be conveniently constructed with NewUserMsg and AddTo or WithUserMsg
// see the HasUserMsg documentation for alternatives.
type UserMsgErrCode struct {
ErrorCode
Msg string
type UserMsgErrCode[Err ErrorCode] struct {
ErrorCode Err
Msg string
}

// Unwrap satisfies the errors package Unwrap function
func (e UserMsgErrCode) Unwrap() error {
func (e UserMsgErrCode[Err]) Unwrap() error {
return e.ErrorCode
}

// Error prefixes the user message to the underlying Err Error.
func (e UserMsgErrCode) Error() string {
func (e UserMsgErrCode[Err]) Error() string {
return e.Msg + ": " + e.ErrorCode.Error()
}

func (e UserMsgErrCode[Err]) Code() Code {
return e.ErrorCode.Code()
}

// GetUserMsg satisfies the [HasUserMsg] interface.
func (e UserMsgErrCode) GetUserMsg() string {
func (e UserMsgErrCode[Err]) GetUserMsg() string {
return e.Msg
}

var _ ErrorCode = (*UserMsgErrCode)(nil) // assert implements interface
var _ HasUserMsg = (*UserMsgErrCode)(nil) // assert implements interface
var _ unwrapError = (*UserMsgErrCode)(nil) // assert implements interface
var _ ErrorCode = (*UserMsgErrCode[ErrorCode])(nil) // assert implements interface
var _ HasUserMsg = (*UserMsgErrCode[ErrorCode])(nil) // assert implements interface
var _ unwrapError = (*UserMsgErrCode[ErrorCode])(nil) // assert implements interface

// AddUserMsg is constructed by UserMsg. It allows method chaining with AddTo.
type AddUserMsg func(ErrorCode) UserCode
Expand All @@ -99,15 +103,20 @@ func (add AddUserMsg) AddTo(err ErrorCode) UserCode {
// }
func UserMsg(msg string) AddUserMsg {
return func(err ErrorCode) UserCode {
return WithUserMsg(msg, err)
res := WithUserMsg(msg, err)
// return a nil UserCode rather than a nil UserCode(*UserMsgErrCode)
if res == nil {
return nil
}
return res
}
}

// WithUserMsg creates a UserMsgErrCode
// Panics if msg is empty or err is nil.
func WithUserMsg(msg string, err ErrorCode) UserCode {
if err == nil {
func WithUserMsg[Err ErrorCode](msg string, err Err) *UserMsgErrCode[Err] {
if ErrorCode(err) == nil {
return nil
}
return UserMsgErrCode{Msg: msg, ErrorCode: err}
return &UserMsgErrCode[Err]{Msg: msg, ErrorCode: err}
}
42 changes: 26 additions & 16 deletions wrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,18 @@ func Wraps(errCode ErrorCode, msg string, args ...interface{}) ErrorCode {
// WrapUser calls errors.Wrap on the inner error.
// This will wrap in place via errors.ErrorWrapper if available
// If a nil is given it is a noop
func WrapUser(errCode UserCode, msg string) UserCode {
//nolint:govet
return wrapG(wrapUserWith, errCode, msg)
func WrapUser[Err UserCode](errCode Err, msg string) *wrappedUserCode[Err] {
return wrapUserWith(errCode, errors.WrapFn(msg))
}

// WrapUserf calls errors.Wrapf on the inner error.
// This will wrap in place via errors.ErrorWrapper if available
// If a nil is given it is a noop
func WrapfUser(errCode UserCode, msg string, args ...interface{}) UserCode {
return wrapG(wrapUserWith, errCode, msg, args...)
func WrapfUser[Err UserCode](errCode Err, msg string, args ...interface{}) *wrappedUserCode[Err] {
if len(args) == 0 {
return wrapUserWith(errCode, errors.WrapFn(msg))
}
return wrapUserWith(errCode, errors.WrapfFn(msg, args...))
}

// WrapsUser calls errors.Wraps on the inner error.
Expand Down Expand Up @@ -78,18 +80,22 @@ func wrapWith(errCode ErrorCode, wrap func(error) error) ErrorCode {
if ok {
return errCode
}
return wrappedErrorCode{newWithError(errCode, wrap)}
return wrappedErrorCode[ErrorCode]{newWithError(errCode, wrap)}
}

func wrapUserWith(errCode UserCode, wrap func(error) error) UserCode {
if errCode == nil {
return errCode
func id[T any](x T) T {
return x
}

func wrapUserWith[Err UserCode](errCode Err, wrap func(error) error) *wrappedUserCode[Err] {
if UserCode(errCode) == nil {
return nil
}
ok := errors.WrapInPlace(errCode, wrap)
if ok {
return errCode
return &wrappedUserCode[Err]{newWithError(errCode, id)}
}
return wrappedUserCode{newWithError(errCode, wrap)}
return &wrappedUserCode[Err]{newWithError(errCode, wrap)}
}

func wrapOpWith(errCode OpCode, wrap func(error) error) OpCode {
Expand Down Expand Up @@ -124,19 +130,23 @@ func newWithError[Err error](errCode Err, wrapErr func(error) error) withError[E
}
}

type wrappedErrorCode struct{ withError[ErrorCode] }
type wrappedErrorCode[Err ErrorCode] struct{ withError[ErrorCode] }

func (wec wrappedErrorCode) Code() Code {
func (wec wrappedErrorCode[Err]) Code() Code {
return wec.With.Code()
}

type wrappedUserCode struct{ withError[UserCode] }
type wrappedUserCode[Err UserCode] struct{ withError[Err] }

func (wec wrappedUserCode) Code() Code {
func (wec wrappedUserCode[Err]) Code() Code {
return wec.With.Code()
}

func (wec wrappedUserCode) GetUserMsg() string {
func (wec wrappedUserCode[Err]) Unwrap() error {
return wec.With
}

func (wec wrappedUserCode[Err]) GetUserMsg() string {
return wec.With.GetUserMsg()
}

Expand Down
36 changes: 18 additions & 18 deletions wrap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,46 +74,46 @@ func TestUserWrapperFunctions(t *testing.T) {
errcode.NewBadRequestErr(underlying),
)
AssertCode(t, coded, errcode.InvalidInputCode.CodeStr())
coded = errcode.WrapUser(coded, "wrapped")
AssertCode(t, coded, errcode.InvalidInputCode.CodeStr())
if errMsg := coded.Error(); errMsg != "user: wrapped: underlying" {
userCode := errcode.WrapUser(coded, "wrapped")
AssertCode(t, userCode, errcode.InvalidInputCode.CodeStr())
if errMsg := userCode.Error(); errMsg != "user: wrapped: underlying" {
t.Errorf("Wrap unexpected: %s", errMsg)
}
tripleUnwrap := errors.Unwrap(errors.Unwrap(errors.Unwrap(coded)))
if tripleUnwrap.Error() != underlying.Error() {
t.Errorf("bad unwrap: %s", tripleUnwrap.Error())
cause := errors.Cause(userCode)
if cause.Error() != underlying.Error() {
t.Errorf("bad unwrap: %s. expected: %s", cause.Error(), underlying.Error())
}
}

{
coded := errcode.WithUserMsg("user",
orig := errcode.WithUserMsg("user",
errcode.NewBadRequestErr(underlying),
)
AssertCode(t, coded, errcode.InvalidInputCode.CodeStr())
coded = errcode.WrapfUser(coded, "wrapped %s", "arg")
AssertCode(t, orig, errcode.InvalidInputCode.CodeStr())
coded := errcode.WrapfUser(orig, "wrapped %s", "arg")
AssertCode(t, coded, errcode.InvalidInputCode.CodeStr())
if errMsg := coded.Error(); errMsg != "user: wrapped arg: underlying" {
t.Errorf("Wrap unexpected: %s", errMsg)
}
tripleUnwrap := errors.Unwrap(errors.Unwrap(errors.Unwrap(coded)))
if tripleUnwrap.Error() != underlying.Error() {
t.Errorf("bad unwrap: %s", tripleUnwrap.Error())
cause := errors.Cause(coded)
if cause.Error() != underlying.Error() {
t.Errorf("bad unwrap: %s", cause.Error())
}
}

{
coded := errcode.WithUserMsg("user",
orig := errcode.WithUserMsg("user",
errcode.NewBadRequestErr(underlying),
)
AssertCode(t, coded, errcode.InvalidInputCode.CodeStr())
coded = errcode.WrapsUser(coded, "wrapped", "arg", 1)
AssertCode(t, orig, errcode.InvalidInputCode.CodeStr())
coded := errcode.WrapsUser(orig, "wrapped", "arg", 1)
AssertCode(t, coded, errcode.InvalidInputCode.CodeStr())
if errMsg := coded.Error(); errMsg != "user: wrapped arg=1: underlying" {
t.Errorf("Wrap unexpected: %s", errMsg)
}
tripleUnwrap := errors.Unwrap(errors.Unwrap(errors.Unwrap(coded)))
if tripleUnwrap.Error() != underlying.Error() {
t.Errorf("bad unwrap: %s", tripleUnwrap.Error())
cause := errors.Cause(coded)
if cause.Error() != underlying.Error() {
t.Errorf("bad unwrap: %s. expected: %s", cause.Error(), underlying.Error())
}
}
}
Expand Down

0 comments on commit d7839dd

Please sign in to comment.