Skip to content

Commit

Permalink
feat: add Join for multierror support
Browse files Browse the repository at this point in the history
  • Loading branch information
jsteenb2 committed Mar 18, 2024
1 parent 8ff6074 commit 6a7afb0
Show file tree
Hide file tree
Showing 7 changed files with 465 additions and 22 deletions.
83 changes: 63 additions & 20 deletions err_impl.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
package errors

import (
"errors"
"cmp"
)

func newE(opts ...any) error {
var err e

skipFrames := FrameSkips(3)
for _, o := range opts {
if o == nil {
continue
}
switch arg := o.(type) {
case string:
err.msg = arg
Expand Down Expand Up @@ -83,19 +86,22 @@ func (err *e) Fields() []any {
out []any
kind Kind
)
for err := error(err); err != nil; err = errors.Unwrap(err) {
ee, ok := err.(*e)
if !ok {
continue
}

for _, kv := range ee.kvs {
for err := error(err); err != nil; err = Unwrap(err) {
em := getErrMeta(err)
for _, kv := range em.kvs {
out = append(out, kv.K, kv.V)
}
if kind == "" {
kind = ee.kind
kind = cmp.Or(kind, em.kind)
if ej, ok := err.(*joinE); ok {
innerKind, multiErrFields := ej.subErrFields()
kind = cmp.Or(kind, innerKind)
if len(multiErrFields) > 0 {
out = append(out, "multi_err", multiErrFields)
}
break
}
}

if kind != "" {
out = append(out, "err_kind", string(kind))
}
Expand All @@ -110,18 +116,18 @@ func (err *e) Fields() []any {
return out
}

func (err *e) Is(target error) bool {
kind, ok := target.(Kind)
return ok && err.kind == kind
}

func (err *e) Unwrap() error {
return err.wrappedErr
}

func (err *e) V(key string) (any, bool) {
for err := error(err); err != nil; err = errors.Unwrap(err) {
ee, ok := err.(*e)
if !ok {
continue
}

for _, kv := range ee.kvs {
for err := error(err); err != nil; err = Unwrap(err) {
for _, kv := range getErrMeta(err).kvs {
if kv.K == key {
return kv.V, true
}
Expand All @@ -132,10 +138,47 @@ func (err *e) V(key string) (any, bool) {

func (err *e) stackTrace() StackFrames {
var out StackFrames
for err := error(err); err != nil; err = errors.Unwrap(err) {
if ee, ok := err.(*e); ok && ee.frame.FilePath != "" {
out = append(out, ee.frame)
for err := error(err); err != nil; err = Unwrap(err) {
em := getErrMeta(err)
if em.frame.FilePath == "" {
continue
}
out = append(out, em.frame)
if em.errType == errTypeJoin {
break
}
}
return out
}

type errMeta struct {
kind Kind
frame Frame
kvs []KV
errType string
}

const (
errTypeE = "e"
errTypeJoin = "j"
)

func getErrMeta(err error) errMeta {
var em errMeta
switch err := err.(type) {
case *e:
em.kind, em.frame, em.kvs, em.errType = err.kind, err.frame, err.kvs, errTypeE
case *joinE:
em.kind, em.frame, em.kvs, em.errType = err.kind, err.frame, err.kvs, errTypeJoin
}
return em
}

func getKind(err error) Kind {
for ; err != nil; err = Unwrap(err) {
if em := getErrMeta(err); em.kind != "" {
return em.kind
}
}
return ""
}
14 changes: 13 additions & 1 deletion errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,16 @@ func Wrap(err error, opts ...any) error {
return newE(passedOpts...)
}

// Join returns a new multi error.
//
// TODO:
// - play with Join(opts ...any) and Join(errs []error, opts ...any) sigs
// and ask for feedback regarding tradeoffs with type safety of first arg.
// As of writing some tests, I kind of dig the loose Join(opts ...any).
func Join(opts ...any) error {
return newJoinE(opts...)
}

// Fields returns logging fields for a given error.
func Fields(err error) []any {
if err == nil {
Expand All @@ -41,8 +51,10 @@ func Fields(err error) []any {
// TODO:
// 1. make this more robust with Is
// 2. determine if its even worth exposing an accessor for this private method
// 3. allow for StackTraces() to accommodate joined errors, perhaps returning a map[string]StackFrames
// or some graph representation would be awesome.
func StackTrace(err error) StackFrames {
ee, ok := err.(*e)
ee, ok := err.(interface{ stackTrace() StackFrames })
if !ok {
return nil
}
Expand Down
37 changes: 37 additions & 0 deletions errors_stack_traces_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,43 @@ func Test_Errors(t *testing.T) {
},
},
},
{
name: "with wrapped joined errors error with inner kind",
input: errors.Wrap(
errors.Wrap(
errors.Join(
errors.Wrap(
errors.New("first error", errors.Kind("inner")),
),
errors.New("second error"),
),
),
),
want: wants{
msg: `2 errors occurred:
* first error
* second error
`,
fields: []any{
"multi_err", []any{
"err_0", []any{
"err_kind", "inner",
"stack_trace", []string{
"github.com/jsteenb2/errors/errors_stack_traces_test.go:189[Test_Errors]",
"github.com/jsteenb2/errors/errors_stack_traces_test.go:190[Test_Errors]",
},
},
"err_1", []any{"stack_trace", []string{"github.com/jsteenb2/errors/errors_stack_traces_test.go:192[Test_Errors]"}},
},
"err_kind", "inner",
"stack_trace", []string{
"github.com/jsteenb2/errors/errors_stack_traces_test.go:186[Test_Errors]",
"github.com/jsteenb2/errors/errors_stack_traces_test.go:187[Test_Errors]",
"github.com/jsteenb2/errors/errors_stack_traces_test.go:188[Test_Errors]",
},
},
},
},
}

for _, tt := range tests {
Expand Down
8 changes: 8 additions & 0 deletions errors_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package errors_test

import (
"encoding/json"
"reflect"
"testing"

Expand Down Expand Up @@ -77,6 +78,13 @@ func eqV[T comparable](t *testing.T, err error, key string, want T) bool {
func eqFields(t *testing.T, want, got []any) bool {
t.Helper()

defer func() {
if t.Failed() {
b, _ := json.MarshalIndent(got, "", " ")
t.Logf("got: %s", string(b))
}
}()

if matches := eqLen(t, len(want), got); !matches {
return matches
}
Expand Down
Loading

0 comments on commit 6a7afb0

Please sign in to comment.