Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add Join/Disjoin support #6

Merged
merged 4 commits into from
Mar 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading