Skip to content

Commit

Permalink
error wrapping that maintains types
Browse files Browse the repository at this point in the history
  • Loading branch information
gregwebs committed Oct 1, 2024
1 parent baa1a68 commit f74dab5
Show file tree
Hide file tree
Showing 8 changed files with 165 additions and 351 deletions.
56 changes: 35 additions & 21 deletions codes.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ type CodedError struct {
Err error
}

func (ce *CodedError) WrapError(apply func(error) error) {
ce.Err = apply(ce.Err)
}

// NewCodedError is for constructing broad error kinds (e.g. those representing HTTP codes)
// Which could have many different underlying go errors.
// Eventually you may want to give your go errors more specific codes.
Expand Down Expand Up @@ -116,26 +120,28 @@ func (e CodedError) Code() Code {
}

// invalidInputErr gives the code InvalidInputCode.
type invalidInputErr struct{ CodedError }
type invalidInputErr struct{ *CodedError }

// NewInvalidInputErr creates an invalidInputErr from an err.
// If the error is already an ErrorCode it will use that code.
// Otherwise it will use InvalidInputCode which gives HTTP 400.
func NewInvalidInputErr(err error) ErrorCode {
return invalidInputErr{NewCodedError(err, InvalidInputCode)}
coded := NewCodedError(err, InvalidInputCode)
return invalidInputErr{&coded}
}

var _ ErrorCode = (*invalidInputErr)(nil) // assert implements interface
var _ unwrapError = (*invalidInputErr)(nil) // assert implements interface

// badReqeustErr gives the code BadRequestErr.
type BadRequestErr struct{ CodedError }
type BadRequestErr struct{ *CodedError }

// NewBadRequestErr creates a BadReqeustErr from an err.
// If the error is already an ErrorCode it will use that code.
// Otherwise it will use BadRequestCode which gives HTTP 400.
func NewBadRequestErr(err error) BadRequestErr {
return BadRequestErr{NewCodedError(err, InvalidInputCode)}
coded := NewCodedError(err, InvalidInputCode)
return BadRequestErr{&coded}
}

// InternalErr gives the code InternalCode
Expand Down Expand Up @@ -171,7 +177,7 @@ func makeInternalStackCode(defaultCode Code) func(error) StackCode {
code = errCode
}
}
return NewStackCode(CodedError{GetCode: code, Err: err}, 3)
return NewStackCode(&CodedError{GetCode: code, Err: err}, 3)
}
}

Expand Down Expand Up @@ -202,89 +208,97 @@ func NewUnavailableErr(err error) UnavailableErr {
}

// notFound gives the code NotFoundCode.
type NotFoundErr struct{ CodedError }
type NotFoundErr struct{ *CodedError }

// NewNotFoundErr creates a notFound from an err.
// If the error is already an ErrorCode it will use that code.
// Otherwise it will use NotFoundCode which gives HTTP 404.
func NewNotFoundErr(err error) NotFoundErr {
return NotFoundErr{NewCodedError(err, NotFoundCode)}
coded := NewCodedError(err, NotFoundCode)
return NotFoundErr{&coded}
}

var _ ErrorCode = (*NotFoundErr)(nil) // assert implements interface
var _ unwrapError = (*NotFoundErr)(nil) // assert implements interface

// NotAuthenticatedErr gives the code NotAuthenticatedCode.
type NotAuthenticatedErr struct{ CodedError }
type NotAuthenticatedErr struct{ *CodedError }

// NewNotAuthenticatedErr creates a NotAuthenticatedErr from an err.
// If the error is already an ErrorCode it will use that code.
// Otherwise it will use NotAuthenticatedCode which gives HTTP 401.
func NewNotAuthenticatedErr(err error) NotAuthenticatedErr {
return NotAuthenticatedErr{NewCodedError(err, NotAuthenticatedCode)}
coded := NewCodedError(err, NotAuthenticatedCode)
return NotAuthenticatedErr{&coded}
}

var _ ErrorCode = (*NotAuthenticatedErr)(nil) // assert implements interface
var _ unwrapError = (*NotAuthenticatedErr)(nil) // assert implements interface

// ForbiddenErr gives the code ForbiddenCode.
type ForbiddenErr struct{ CodedError }
type ForbiddenErr struct{ *CodedError }

// NewForbiddenErr creates a ForbiddenErr from an err.
// If the error is already an ErrorCode it will use that code.
// Otherwise it will use ForbiddenCode which gives HTTP 401.
func NewForbiddenErr(err error) ForbiddenErr {
return ForbiddenErr{NewCodedError(err, ForbiddenCode)}
coded := NewCodedError(err, ForbiddenCode)
return ForbiddenErr{&coded}
}

var _ ErrorCode = (*ForbiddenErr)(nil) // assert implements interface
var _ unwrapError = (*ForbiddenErr)(nil) // assert implements interface

// UnprocessableErr gives the code UnprocessibleCode.
type UnprocessableErr struct{ CodedError }
type UnprocessableErr struct{ *CodedError }

// NewUnprocessableErr creates an UnprocessableErr from an err.
// If the error is already an ErrorCode it will use that code.
// Otherwise it will use UnprocessableEntityCode which gives HTTP 422.
func NewUnprocessableErr(err error) UnprocessableErr {
return UnprocessableErr{NewCodedError(err, UnprocessableEntityCode)}
coded := NewCodedError(err, UnprocessableEntityCode)
return UnprocessableErr{&coded}
}

// NotAcceptableErr gives the code NotAcceptableCode.
type NotAcceptableErr struct{ CodedError }
type NotAcceptableErr struct{ *CodedError }

// NewUnprocessableErr creates an UnprocessableErr from an err.
// If the error is already an ErrorCode it will use that code.
// Otherwise it will use NotAcceptableCode which gives HTTP 406.
func NewNotAcceptableErr(err error) NotAcceptableErr {
return NotAcceptableErr{NewCodedError(err, NotAcceptableCode)}
coded := NewCodedError(err, NotAcceptableCode)
return NotAcceptableErr{&coded}
}

type AlreadyExistsErr struct{ CodedError }
type AlreadyExistsErr struct{ *CodedError }

// NewAlreadyExistsErr creates an AlreadyExistsErr from an err.
// If the error is already an ErrorCode it will use that code.
// Otherwise it will use AlreadyExistsCode which gives HTTP 409.
func NewAlreadyExistsErr(err error) AlreadyExistsErr {
return AlreadyExistsErr{NewCodedError(err, AlreadyExistsCode)}
coded := NewCodedError(err, AlreadyExistsCode)
return AlreadyExistsErr{&coded}
}

// TimeoutGatewayErr gives the code TimeoutGatewayCode
type TimeoutGatewayErr struct{ CodedError }
type TimeoutGatewayErr struct{ *CodedError }

// NewTimeoutGatewayErr creates a TimeoutGatewayErr from an err.
// If the error is already an ErrorCode it will use that code.
// Otherwise it will use TimeoutGatewayErr which gives HTTP 504.
func NewTimeoutGatewayErr(err error) TimeoutGatewayErr {
return TimeoutGatewayErr{NewCodedError(err, TimeoutGatewayCode)}
coded := NewCodedError(err, TimeoutGatewayCode)
return TimeoutGatewayErr{&coded}
}

// TimeoutRequestErr gives the code TimeoutRequestCode
type TimeoutRequestErr struct{ CodedError }
type TimeoutRequestErr struct{ *CodedError }

// NewTimeoutRequestErr creates a TimeoutRequestErr from an err.
// If the error is already an ErrorCode it will use that code.
// Otherwise it will use TimeoutRequestErr which gives HTTP 408.
func NewTimeoutRequestErr(err error) TimeoutRequestErr {
return TimeoutRequestErr{NewCodedError(err, TimeoutRequestCode)}
coded := NewCodedError(err, TimeoutRequestCode)
return TimeoutRequestErr{&coded}
}
138 changes: 46 additions & 92 deletions error_code.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,120 +120,74 @@ func (code Code) IsAncestor(ancestorCode Code) bool {
return nil != code.findAncestor(func(an Code) bool { return an == ancestorCode })
}

// ErrorCode is the interface that ties an error and RegisteredCode together.
// ErrorCode is the interface that ties an error and Code together.
// A Function that is not written in the context of an (HTTP) handler
// can return a code that will eventually be sent back to the client.
//
// Note that there are additional interfaces (HasClientData, HasOperation, please see the docs)
// Note that there are additional interfaces such as UserCode
// that can be defined by an ErrorCode to customize finding structured data for the client.
//
// ErrorCode allows error codes to be defined
// without being forced to use a particular struct such as CodedError.
// CodedError is convenient for generic errors that wrap many different errors with similar codes.
// Please see the docs for CodedError.
// For an application specific error with a 1:1 mapping between a go error structure and a RegisteredCode,
// You probably want to use this interface directly. Example:
// The ErrorCode interface allows error codes to be defined.
// without being forced to use a particular struct implementation such as CodedError.
// However, CodedError is normally be used for generic error codes that wrap many different errors with the same code.
//
// // First define a normal error type
// type PathBlocked struct {
// start uint64 `json:"start"`
// end uint64 `json:"end"`
// obstacle uint64 `json:"end"`
// }
//
// func (e PathBlocked) Error() string {
// return fmt.Sprintf("The path %d -> %d has obstacle %d", e.start, e.end, e.obstacle)
// }
//
// // Now define the code
// var PathBlockedCode = errcode.StateCode.Child("state.blocked")
//
// // Now attach the code to the error type
// func (e PathBlocked) Code() Code {
// return PathBlockedCode
// }
// The WrapError method allows for modifying the inner error while maintaining the same outer type.
// This is most useful when wrapping an extended ErrorCode interface such as UserCode.
// These are used by the errcode.Wrap* functions.
type ErrorCode interface {
Code() Code
error
Code() Code
}

type ErrorWrap[Err error] interface {
WrapError(func(error) error) Err
}

// unwrapper allows the abstract retrieval of the underlying error.
// unwrapError allows the abstract retrieval of the underlying error.
// Formalize the Unwrap interface, but don't export it.
// The standard library errors package should export it.
// Types that wrap errors should implement this to allow viewing of the underlying error.
type unwrapError interface {
Unwrap() error
}

type Unwrapper[T any] interface {
Unwrapped() T
}

type ErrorCodeWrap[Wrap ErrorCode] interface {
ErrorCode
Unwrapper[Wrap]
}

// wrappedErrorCode is a convenience to maintain the ErrorCode type when wrapping errors
type wrappedErrorCode[Wrapped ErrorCode] struct {
Err error
ErrorCode Wrapped
}

// Code fulfills the ErrorCode interface
func (wrapped wrappedErrorCode[Wrapped]) Code() Code {
return wrapped.ErrorCode.Code()
}

// Error fulfills the ErrorCode interface
func (wrapped wrappedErrorCode[Wrapped]) Error() string {
return wrapped.Err.Error()
}

// Allow unwrapping
func (wrapped wrappedErrorCode[Wrapped]) Unwrap() error {
return wrapped.ErrorCode
}

func (wrapped wrappedErrorCode[Wrapped]) Unwrapped() Wrapped {
return wrapped.ErrorCode
}

// Wrap is a convenience that calls errors.Wrap but still returns the ErrorCode interface
// If a nil ErrorCode is given it will be returned as nil
func Wrap[EC ErrorCode](errCode EC, msg string) ErrorCodeWrap[EC] {
err := errors.Wrap(errCode, msg)
if err == nil {
return nil
}
return wrappedErrorCode[EC]{
Err: err,
ErrorCode: errCode,
// Wrap calls errors.Wrap on the inner error.
// This uses the WrapError method of ErrorWrap
// If a nil is given it is a noop
func Wrap[Err error](errCode ErrorWrap[Err], msg string) Err {
if errCode == nil {
var zero Err
return zero
}
return errCode.WrapError(func(err error) error {
return errors.Wrap(err, msg)
})
}

// Wrapf is a convenience that calls errors.Wrapf but still returns the ErrorCode interface
// If a nil ErrorCode is given it will be returned as nil
func Wrapf[EC ErrorCode](errCode EC, msg string, args ...interface{}) ErrorCodeWrap[EC] {
err := errors.Wrapf(errCode, msg, args...)
if err == nil {
return nil
}
return wrappedErrorCode[EC]{
Err: err,
ErrorCode: errCode,
// Wrap calls errors.Wrapf on the inner error.
// This uses the WrapError method of ErrorWrap
// If a nil is given it is a noop
func Wrapf[Err error](errCode ErrorWrap[Err], msg string, args ...interface{}) Err {
if errCode == nil {
var zero Err
return zero
}
return errCode.WrapError(func(err error) error {
return errors.Wrapf(err, msg, args...)
})
}

// Wraps is a convenience that calls errors.Wraps but still returns the ErrorCode interface
// If a nil ErrorCode is given it will be returned as nil
func Wraps[EC ErrorCode](errCode EC, msg string, args ...interface{}) ErrorCodeWrap[EC] {
err := errors.Wraps(errCode, msg, args...)
if err == nil {
return nil
}
return wrappedErrorCode[EC]{
Err: err,
ErrorCode: errCode,
// Wraps calls errors.Wraps on the inner error.
// This uses the WrapError method of ErrorWrap
// If a nil is given it is a noop
func Wraps[Err error](errCode ErrorWrap[Err], msg string, args ...interface{}) Err {
if errCode == nil {
var zero Err
return zero
}
return errCode.WrapError(func(err error) error {
return errors.Wraps(err, msg, args...)
})
}

// HasClientData is used to defined how to retrieve the data portion of an ErrorCode to be returned to the client.
Expand Down
Loading

0 comments on commit f74dab5

Please sign in to comment.