Skip to content

Commit

Permalink
Merge pull request #6 from jsteenb2/feat/multierr
Browse files Browse the repository at this point in the history
feat: add Join/Disjoin support
  • Loading branch information
jsteenb2 authored Mar 18, 2024
2 parents 7bb1e7d + c6c0238 commit ac41a4e
Show file tree
Hide file tree
Showing 10 changed files with 541 additions and 27 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.20'
go-version: '1.22'

- name: Test
run: go test -trimpath -v ./...
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 ""
}
31 changes: 30 additions & 1 deletion errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,33 @@ 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...)
}

// Disjoin separates joined errors.
func Disjoin(err error) []error {
if err == nil {
return nil
}

if stdJoin, ok := err.(interface{ Unwrap() []error }); ok {
return stdJoin.Unwrap()
}

if ej, ok := err.(*joinE); ok {
return ej.errs
}

return nil
}

// Fields returns logging fields for a given error.
func Fields(err error) []any {
if err == nil {
Expand All @@ -41,8 +68,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
13 changes: 10 additions & 3 deletions errors_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package errors_test

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

Expand All @@ -12,12 +12,12 @@ func TestWrap(t *testing.T) {
t.Run("simple wrapped error is returned when calling std lib errors.Unwrap", func(t *testing.T) {
baseErr := errors.New("first error")

if unwrapped := stderrors.Unwrap(baseErr); unwrapped != nil {
if unwrapped := errors.Unwrap(baseErr); unwrapped != nil {
t.Fatalf("recieved unexpected unwrapped error:\n\t\tgot:\t%v", unwrapped)
}

wrappedErr := errors.Wrap(baseErr)
if unwrapped := stderrors.Unwrap(wrappedErr); unwrapped == nil {
if unwrapped := errors.Unwrap(wrappedErr); unwrapped == nil {
t.Fatalf("recieved unexpected nil unwrapped error")
}
})
Expand Down Expand Up @@ -78,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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module github.com/jsteenb2/errors

go 1.20
go 1.22
Loading

0 comments on commit ac41a4e

Please sign in to comment.