Skip to content

Commit 6adb34f

Browse files
committed
Implement custom encode/decode for multi-cause errors
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.
1 parent 042d819 commit 6adb34f

16 files changed

+8601
-2
lines changed

errbase/decode.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,15 @@ func decodeLeaf(ctx context.Context, enc *errorspb.EncodedErrorLeaf) error {
5757
return genErr
5858
}
5959
// Decoding failed, we'll drop through to opaqueLeaf{} below.
60+
} else if decoder, ok := multiCauseDecoders[typeKey]; ok {
61+
causes := make([]error, len(enc.MultierrorCauses))
62+
for i, e := range enc.MultierrorCauses {
63+
causes[i] = DecodeError(ctx, *e)
64+
}
65+
genErr := decoder(ctx, causes, enc.Message, enc.Details.ReportablePayload, payload)
66+
if genErr != nil {
67+
return genErr
68+
}
6069
} else {
6170
// Shortcut for non-registered proto-encodable error types:
6271
// if it already implements `error`, it's good to go.
@@ -174,3 +183,24 @@ type WrapperDecoder = func(ctx context.Context, cause error, msgPrefix string, s
174183

175184
// registry for RegisterWrapperType.
176185
var decoders = map[TypeKey]WrapperDecoder{}
186+
187+
// MultiCauseDecoder is to be provided (via RegisterMultiCauseDecoder
188+
// above) by additional multi-cause wrapper types not yet known by the
189+
// library. A nil return indicates that decoding was not successful.
190+
type MultiCauseDecoder = func(ctx context.Context, causes []error, msgPrefix string, safeDetails []string, payload proto.Message) error
191+
192+
// registry for RegisterMultiCauseDecoder.
193+
var multiCauseDecoders = map[TypeKey]MultiCauseDecoder{}
194+
195+
// RegisterMultiCauseDecoder can be used to register new multi-cause
196+
// wrapper types to the library. Registered wrappers will be decoded
197+
// using their own Go type when an error is decoded. Multi-cause
198+
// wrappers that have not been registered will be decoded using the
199+
// opaqueWrapper type.
200+
func RegisterMultiCauseDecoder(theType TypeKey, decoder MultiCauseDecoder) {
201+
if decoder == nil {
202+
delete(multiCauseDecoders, theType)
203+
} else {
204+
multiCauseDecoders[theType] = decoder
205+
}
206+
}

errbase/encode.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,28 @@ type LeafEncoder = func(ctx context.Context, err error) (msg string, safeDetails
328328
// registry for RegisterLeafEncoder.
329329
var leafEncoders = map[TypeKey]LeafEncoder{}
330330

331+
// RegisterMultiCauseEncoder can be used to register new multi-cause
332+
// error types to the library. Registered types will be encoded using
333+
// their own Go type when an error is encoded. Multi-cause wrappers
334+
// that have not been registered will be encoded using the
335+
// opaqueWrapper type.
336+
func RegisterMultiCauseEncoder(theType TypeKey, encoder MultiCauseEncoder) {
337+
// This implementation is a simple wrapper around `LeafEncoder`
338+
// because we implemented multi-cause error wrapper encoding into a
339+
// `Leaf` instead of a `Wrapper` for smoother backwards
340+
// compatibility support. Exposing this detail to consumers of the
341+
// API is confusing and hence avoided. The causes of the error are
342+
// encoded separately regardless of this encoder's implementation.
343+
RegisterLeafEncoder(theType, encoder)
344+
}
345+
346+
// MultiCauseEncoder is to be provided (via RegisterMultiCauseEncoder
347+
// above) by additional multi-cause wrapper types not yet known to this
348+
// library. The encoder will automatically extract and encode the
349+
// causes of this error by calling `Unwrap()` and expecting a slice of
350+
// errors.
351+
type MultiCauseEncoder = func(ctx context.Context, err error) (msg string, safeDetails []string, payload proto.Message)
352+
331353
// RegisterWrapperEncoder can be used to register new wrapper types to
332354
// the library. Registered wrappers will be encoded using their own
333355
// Go type when an error is encoded. Wrappers that have not been

errbase/format_error.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -502,7 +502,10 @@ func (s *state) formatRecursive(err error, isOutermost, withDetail, withDepth bo
502502
}
503503

504504
// elideShortChildren takes a number of entries to set `elideShort` to
505-
// false. The reason a number of entries is needed is because
505+
// false. The reason a number of entries is needed is that we may be
506+
// eliding a subtree of causes in the case of a multi-cause error. In
507+
// the multi-cause case, we need to know how many of the prior errors
508+
// in the list of entries is a child of this subtree.
506509
func (s *state) elideShortChildren(newEntries int) {
507510
for i := 0; i < newEntries; i++ {
508511
s.entries[len(s.entries)-1-i].elideShort = true

errbase_api.go

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,20 @@ func GetTypeKey(err error) TypeKey { return errbase.GetTypeKey(err) }
8686
// A nil return indicates that decoding was not successful.
8787
type LeafDecoder = errbase.LeafDecoder
8888

89+
// MultiCauseDecoder is to be provided (via RegisterMultiCauseDecoder
90+
// above) by additional multi-cause wrapper types not yet known by the
91+
// library. A nil return indicates that decoding was not successful.
92+
type MultiCauseDecoder = errbase.MultiCauseDecoder
93+
94+
// RegisterMultiCauseDecoder can be used to register new multi-cause
95+
// wrapper types to the library. Registered wrappers will be decoded
96+
// using their own Go type when an error is decoded. Multi-cause
97+
// wrappers that have not been registered will be decoded using the
98+
// opaqueWrapper type.
99+
func RegisterMultiCauseDecoder(theType TypeKey, decoder MultiCauseDecoder) {
100+
errbase.RegisterMultiCauseDecoder(theType, decoder)
101+
}
102+
89103
// RegisterWrapperDecoder can be used to register new wrapper types to
90104
// the library. Registered wrappers will be decoded using their own
91105
// Go type when an error is decoded. Wrappers that have not been
@@ -145,7 +159,7 @@ type WrapperEncoder = errbase.WrapperEncoder
145159
// Note: if the error type has been migrated from a previous location
146160
// or a different type, ensure that RegisterTypeMigration() was called
147161
// prior to RegisterWrapperEncoder().
148-
func RegisterWrapperEncoderWithMessageType(typeName TypeKey, encoder errbase.WrapperEncoderWithMessageType) {
162+
func RegisterWrapperEncoderWithMessageType(typeName TypeKey, encoder WrapperEncoderWithMessageType) {
149163
errbase.RegisterWrapperEncoderWithMessageType(typeName, encoder)
150164
}
151165

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

171+
// RegisterMultiCauseEncoder can be used to register new multi-cause
172+
// error types to the library. Registered types will be encoded using
173+
// their own Go type when an error is encoded. Multi-cause wrappers
174+
// that have not been registered will be encoded using the
175+
// opaqueWrapper type.
176+
func RegisterMultiCauseEncoder(typeName TypeKey, encoder MultiCauseEncoder) {
177+
errbase.RegisterMultiCauseEncoder(typeName, encoder)
178+
}
179+
180+
// MultiCauseEncoder is to be provided (via RegisterMultiCauseEncoder
181+
// above) by additional multi-cause wrapper types not yet known to this
182+
// library. The encoder will automatically extract and encode the
183+
// causes of this error by calling `Unwrap()` and expecting a slice of
184+
// errors.
185+
type MultiCauseEncoder = errbase.MultiCauseEncoder
186+
157187
// SetWarningFn enables configuration of the warning function.
158188
func SetWarningFn(fn func(context.Context, string, ...interface{})) { errbase.SetWarningFn(fn) }
159189

fmttests/datadriven_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,20 @@ var wrapCommands = map[string]commandFn{
207207
// werrWithElidedClause overrides its cause's Error() from its own
208208
// short message.
209209
"elided-cause": func(err error, args []arg) error { return &werrWithElidedCause{err, strfy(args)} },
210+
"multi-cause": func(err error, args []arg) error {
211+
return newMultiCause("A", false, /* elide */
212+
newMultiCause("C", false /* elide */, err, errutil.New(strfy(args))),
213+
newMultiCause("B", false /* elide */, errutil.New("included 1"), errutil.New("included 2")),
214+
)
215+
},
216+
// This iteration elides the causes in the second child error,
217+
// which omits them from the format string.
218+
"multi-elided-cause": func(err error, args []arg) error {
219+
return newMultiCause("A", false, /* elide */
220+
newMultiCause("C", false /* elide */, err, errutil.New(strfy(args))),
221+
newMultiCause("B", true /* elide */, errutil.New("elided 1"), errutil.New("elided 2")),
222+
)
223+
},
210224

211225
// stack attaches a simple stack trace.
212226
"stack": func(err error, _ []arg) error { return withstack.WithStack(err) },

fmttests/format_error_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -733,3 +733,55 @@ func (w *werrSafeFormat) SafeFormatError(p errbase.Printer) (next error) {
733733
p.Printf("safe %s", w.msg)
734734
return w.cause
735735
}
736+
737+
type errMultiCause struct {
738+
causes []error
739+
msg string
740+
elide bool
741+
}
742+
743+
func newMultiCause(msg string, elide bool, causes ...error) *errMultiCause {
744+
return &errMultiCause{
745+
causes: causes,
746+
msg: msg,
747+
elide: elide,
748+
}
749+
}
750+
751+
func (e *errMultiCause) Error() string { return fmt.Sprint(e) }
752+
func (e *errMultiCause) Format(s fmt.State, verb rune) { errbase.FormatError(e, s, verb) }
753+
func (e *errMultiCause) SafeFormatError(p errbase.Printer) (next error) {
754+
p.Printf("%s", e.msg)
755+
if e.elide {
756+
return nil
757+
} else {
758+
return e.causes[0]
759+
}
760+
}
761+
func (e *errMultiCause) Unwrap() []error { return e.causes }
762+
763+
func init() {
764+
errbase.RegisterMultiCauseEncoder(errbase.GetTypeKey(&errMultiCause{}), encodeWithMultiCause)
765+
errbase.RegisterMultiCauseDecoder(errbase.GetTypeKey(&errMultiCause{}), decodeWithMultiCause)
766+
}
767+
768+
func encodeWithMultiCause(
769+
_ context.Context, err error,
770+
) (string, []string, proto.Message) {
771+
m := err.(*errMultiCause)
772+
if m.elide {
773+
return m.msg, []string{"elide"}, nil
774+
} else {
775+
return m.msg, nil, nil
776+
}
777+
}
778+
779+
func decodeWithMultiCause(
780+
_ context.Context, causes []error, msg string, details []string, _ proto.Message,
781+
) error {
782+
elide := false
783+
if len(details) == 1 && details[0] == "elide" {
784+
elide = true
785+
}
786+
return &errMultiCause{causes, msg, elide}
787+
}

0 commit comments

Comments
 (0)