Skip to content

Commit

Permalink
Implement custom encode/decode for multi-cause errors
Browse files Browse the repository at this point in the history
Previous support for multi-cause encode/decode functionality, did not
include support for custom encoder and decoder logic.

This commits adds the ability to register encoders and decoders for
multi-cause errors to encode custom types unknown to this library.
  • Loading branch information
dhartunian committed Aug 22, 2023
1 parent 042d819 commit 6adb34f
Show file tree
Hide file tree
Showing 16 changed files with 8,601 additions and 2 deletions.
30 changes: 30 additions & 0 deletions errbase/decode.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,15 @@ func decodeLeaf(ctx context.Context, enc *errorspb.EncodedErrorLeaf) error {
return genErr
}
// Decoding failed, we'll drop through to opaqueLeaf{} below.
} else if decoder, ok := multiCauseDecoders[typeKey]; ok {
causes := make([]error, len(enc.MultierrorCauses))
for i, e := range enc.MultierrorCauses {
causes[i] = DecodeError(ctx, *e)
}
genErr := decoder(ctx, causes, enc.Message, enc.Details.ReportablePayload, payload)
if genErr != nil {
return genErr
}
} else {
// Shortcut for non-registered proto-encodable error types:
// if it already implements `error`, it's good to go.
Expand Down Expand Up @@ -174,3 +183,24 @@ type WrapperDecoder = func(ctx context.Context, cause error, msgPrefix string, s

// registry for RegisterWrapperType.
var decoders = map[TypeKey]WrapperDecoder{}

// MultiCauseDecoder is to be provided (via RegisterMultiCauseDecoder
// above) by additional multi-cause wrapper types not yet known by the
// library. A nil return indicates that decoding was not successful.
type MultiCauseDecoder = func(ctx context.Context, causes []error, msgPrefix string, safeDetails []string, payload proto.Message) error

// registry for RegisterMultiCauseDecoder.
var multiCauseDecoders = map[TypeKey]MultiCauseDecoder{}

// RegisterMultiCauseDecoder can be used to register new multi-cause
// wrapper types to the library. Registered wrappers will be decoded
// using their own Go type when an error is decoded. Multi-cause
// wrappers that have not been registered will be decoded using the
// opaqueWrapper type.
func RegisterMultiCauseDecoder(theType TypeKey, decoder MultiCauseDecoder) {
if decoder == nil {
delete(multiCauseDecoders, theType)
} else {
multiCauseDecoders[theType] = decoder
}
}
22 changes: 22 additions & 0 deletions errbase/encode.go
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,28 @@ type LeafEncoder = func(ctx context.Context, err error) (msg string, safeDetails
// registry for RegisterLeafEncoder.
var leafEncoders = map[TypeKey]LeafEncoder{}

// RegisterMultiCauseEncoder can be used to register new multi-cause
// error types to the library. Registered types will be encoded using
// their own Go type when an error is encoded. Multi-cause wrappers
// that have not been registered will be encoded using the
// opaqueWrapper type.
func RegisterMultiCauseEncoder(theType TypeKey, encoder MultiCauseEncoder) {
// This implementation is a simple wrapper around `LeafEncoder`
// because we implemented multi-cause error wrapper encoding into a
// `Leaf` instead of a `Wrapper` for smoother backwards
// compatibility support. Exposing this detail to consumers of the
// API is confusing and hence avoided. The causes of the error are
// encoded separately regardless of this encoder's implementation.
RegisterLeafEncoder(theType, encoder)
}

// MultiCauseEncoder is to be provided (via RegisterMultiCauseEncoder
// above) by additional multi-cause wrapper types not yet known to this
// library. The encoder will automatically extract and encode the
// causes of this error by calling `Unwrap()` and expecting a slice of
// errors.
type MultiCauseEncoder = func(ctx context.Context, err error) (msg string, safeDetails []string, payload proto.Message)

// RegisterWrapperEncoder can be used to register new wrapper types to
// the library. Registered wrappers will be encoded using their own
// Go type when an error is encoded. Wrappers that have not been
Expand Down
5 changes: 4 additions & 1 deletion errbase/format_error.go
Original file line number Diff line number Diff line change
Expand Up @@ -502,7 +502,10 @@ func (s *state) formatRecursive(err error, isOutermost, withDetail, withDepth bo
}

// elideShortChildren takes a number of entries to set `elideShort` to
// false. The reason a number of entries is needed is because
// false. The reason a number of entries is needed is that we may be
// eliding a subtree of causes in the case of a multi-cause error. In
// the multi-cause case, we need to know how many of the prior errors
// in the list of entries is a child of this subtree.
func (s *state) elideShortChildren(newEntries int) {
for i := 0; i < newEntries; i++ {
s.entries[len(s.entries)-1-i].elideShort = true
Expand Down
32 changes: 31 additions & 1 deletion errbase_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,20 @@ func GetTypeKey(err error) TypeKey { return errbase.GetTypeKey(err) }
// A nil return indicates that decoding was not successful.
type LeafDecoder = errbase.LeafDecoder

// MultiCauseDecoder is to be provided (via RegisterMultiCauseDecoder
// above) by additional multi-cause wrapper types not yet known by the
// library. A nil return indicates that decoding was not successful.
type MultiCauseDecoder = errbase.MultiCauseDecoder

// RegisterMultiCauseDecoder can be used to register new multi-cause
// wrapper types to the library. Registered wrappers will be decoded
// using their own Go type when an error is decoded. Multi-cause
// wrappers that have not been registered will be decoded using the
// opaqueWrapper type.
func RegisterMultiCauseDecoder(theType TypeKey, decoder MultiCauseDecoder) {
errbase.RegisterMultiCauseDecoder(theType, decoder)
}

// RegisterWrapperDecoder can be used to register new wrapper types to
// the library. Registered wrappers will be decoded using their own
// Go type when an error is decoded. Wrappers that have not been
Expand Down Expand Up @@ -145,7 +159,7 @@ type WrapperEncoder = errbase.WrapperEncoder
// Note: if the error type has been migrated from a previous location
// or a different type, ensure that RegisterTypeMigration() was called
// prior to RegisterWrapperEncoder().
func RegisterWrapperEncoderWithMessageType(typeName TypeKey, encoder errbase.WrapperEncoderWithMessageType) {
func RegisterWrapperEncoderWithMessageType(typeName TypeKey, encoder WrapperEncoderWithMessageType) {
errbase.RegisterWrapperEncoderWithMessageType(typeName, encoder)
}

Expand All @@ -154,6 +168,22 @@ func RegisterWrapperEncoderWithMessageType(typeName TypeKey, encoder errbase.Wra
// types not yet known to this library.
type WrapperEncoderWithMessageType = errbase.WrapperEncoderWithMessageType

// RegisterMultiCauseEncoder can be used to register new multi-cause
// error types to the library. Registered types will be encoded using
// their own Go type when an error is encoded. Multi-cause wrappers
// that have not been registered will be encoded using the
// opaqueWrapper type.
func RegisterMultiCauseEncoder(typeName TypeKey, encoder MultiCauseEncoder) {
errbase.RegisterMultiCauseEncoder(typeName, encoder)
}

// MultiCauseEncoder is to be provided (via RegisterMultiCauseEncoder
// above) by additional multi-cause wrapper types not yet known to this
// library. The encoder will automatically extract and encode the
// causes of this error by calling `Unwrap()` and expecting a slice of
// errors.
type MultiCauseEncoder = errbase.MultiCauseEncoder

// SetWarningFn enables configuration of the warning function.
func SetWarningFn(fn func(context.Context, string, ...interface{})) { errbase.SetWarningFn(fn) }

Expand Down
14 changes: 14 additions & 0 deletions fmttests/datadriven_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,20 @@ var wrapCommands = map[string]commandFn{
// werrWithElidedClause overrides its cause's Error() from its own
// short message.
"elided-cause": func(err error, args []arg) error { return &werrWithElidedCause{err, strfy(args)} },
"multi-cause": func(err error, args []arg) error {
return newMultiCause("A", false, /* elide */
newMultiCause("C", false /* elide */, err, errutil.New(strfy(args))),
newMultiCause("B", false /* elide */, errutil.New("included 1"), errutil.New("included 2")),
)
},
// This iteration elides the causes in the second child error,
// which omits them from the format string.
"multi-elided-cause": func(err error, args []arg) error {
return newMultiCause("A", false, /* elide */
newMultiCause("C", false /* elide */, err, errutil.New(strfy(args))),
newMultiCause("B", true /* elide */, errutil.New("elided 1"), errutil.New("elided 2")),
)
},

// stack attaches a simple stack trace.
"stack": func(err error, _ []arg) error { return withstack.WithStack(err) },
Expand Down
52 changes: 52 additions & 0 deletions fmttests/format_error_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -733,3 +733,55 @@ func (w *werrSafeFormat) SafeFormatError(p errbase.Printer) (next error) {
p.Printf("safe %s", w.msg)
return w.cause
}

type errMultiCause struct {
causes []error
msg string
elide bool
}

func newMultiCause(msg string, elide bool, causes ...error) *errMultiCause {
return &errMultiCause{
causes: causes,
msg: msg,
elide: elide,
}
}

func (e *errMultiCause) Error() string { return fmt.Sprint(e) }
func (e *errMultiCause) Format(s fmt.State, verb rune) { errbase.FormatError(e, s, verb) }
func (e *errMultiCause) SafeFormatError(p errbase.Printer) (next error) {
p.Printf("%s", e.msg)
if e.elide {
return nil
} else {
return e.causes[0]
}
}
func (e *errMultiCause) Unwrap() []error { return e.causes }

func init() {
errbase.RegisterMultiCauseEncoder(errbase.GetTypeKey(&errMultiCause{}), encodeWithMultiCause)
errbase.RegisterMultiCauseDecoder(errbase.GetTypeKey(&errMultiCause{}), decodeWithMultiCause)
}

func encodeWithMultiCause(
_ context.Context, err error,
) (string, []string, proto.Message) {
m := err.(*errMultiCause)
if m.elide {
return m.msg, []string{"elide"}, nil
} else {
return m.msg, nil, nil
}
}

func decodeWithMultiCause(
_ context.Context, causes []error, msg string, details []string, _ proto.Message,
) error {
elide := false
if len(details) == 1 && details[0] == "elide" {
elide = true
}
return &errMultiCause{causes, msg, elide}
}
Loading

0 comments on commit 6adb34f

Please sign in to comment.