Skip to content
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
18 changes: 18 additions & 0 deletions .golangci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# https://golangci-lint.run/usage/configuration
version: "2"

linters:
default: standard
enable:
- gocritic
settings:
gocritic:
enable-all: true
exclusions:
presets:
- std-error-handling

formatters:
enable:
- gofumpt
- goimports
23 changes: 0 additions & 23 deletions .golangci.yml

This file was deleted.

6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,9 @@ if pathErr, ok := errorsx.As[*os.PathError](err); ok {
}
```

### Close
### Do

Attempts to close the given `io.Closer` and assigns the returned error (if any) to `err`.
Calls the given function and joins the returned error (if any) with `err`.

```go
f, err := os.Open("file.txt")
Expand All @@ -66,5 +66,5 @@ defer func() {
}()

// After:
defer errorsx.Close(f, &err)
defer errorsx.Do(f.Close, &err)
```
14 changes: 6 additions & 8 deletions errorsx.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
// Package errorsx implements extensions for the standard [errors] package.
package errorsx

import (
"errors"
"io"
)
import "errors"

// IsAny is a multi-target version of [errors.Is].
func IsAny(err, target error, targets ...error) bool {
Expand All @@ -26,8 +23,9 @@ func As[T any](err error) (T, bool) {
return t, ok
}

// Close attempts to close the given [io.Closer] and assigns the returned error (if any) to err.
// If err is already not nil, it will be joined with the [io.Closer]'s error.
func Close(c io.Closer, err *error) { //nolint:gocritic // ptrToRefParam: err must be a pointer here.
*err = errors.Join(*err, c.Close())
// Do calls the given function and joins the returned error (if any) with err.
// Do is meant to be used in defer statements to record errors returned by [os.File.Close], [sql.Tx.Rollback], etc.
// Note that the returned error in the function where Do is deferred must be named.
func Do(fn func() error, err *error) { //nolint:gocritic // ptrToRefParam: err must be a pointer here.
*err = errors.Join(*err, fn())
}
32 changes: 14 additions & 18 deletions errorsx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ func TestIsAny(t *testing.T) {
targets []error
want bool
}{
"no matches": {err: errFoo, targets: []error{errBar}, want: false},
"no match": {err: errFoo, targets: []error{errBar}, want: false},
"single target match": {err: errFoo, targets: []error{errFoo}, want: true},
"single target match (wrapped)": {err: wrap(errFoo), targets: []error{errFoo}, want: true},
"multiple targets match (wrapped)": {err: wrap(errFoo), targets: []error{errBar, errFoo}, want: true},
Expand All @@ -33,41 +33,41 @@ func TestAs(t *testing.T) {
isok := func(_ any, ok bool) bool { return ok }

tests := map[string]struct {
fn func(error) bool
err error
as func(error) bool
want bool
}{
"no match": {fn: func(err error) bool { return isok(errorsx.As[barError](err)) }, err: errFoo, want: false},
"match (exact)": {fn: func(err error) bool { return isok(errorsx.As[fooError](err)) }, err: errFoo, want: true},
"match (wrapped)": {fn: func(err error) bool { return isok(errorsx.As[fooError](err)) }, err: wrap(errFoo), want: true},
"no match": {err: errFoo, as: func(err error) bool { return isok(errorsx.As[barError](err)) }, want: false},
"match (exact)": {err: errFoo, as: func(err error) bool { return isok(errorsx.As[fooError](err)) }, want: true},
"match (wrapped)": {err: wrap(errFoo), as: func(err error) bool { return isok(errorsx.As[fooError](err)) }, want: true},
}

for name, test := range tests {
t.Run(name, func(t *testing.T) {
if got := test.fn(test.err); got != test.want {
if got := test.as(test.err); got != test.want {
t.Errorf("got %t; want %t", got, test.want)
}
})
}
}

func TestClose(t *testing.T) {
func TestDo(t *testing.T) {
tests := map[string]struct {
mainErr error
closeErr error
deferErr error
wantErrs []error
}{
"main: ok; close: ok": {mainErr: nil, closeErr: nil, wantErrs: []error{}},
"main: ok; close: error": {mainErr: nil, closeErr: errBar, wantErrs: []error{errBar}},
"main: error; close: ok": {mainErr: errFoo, closeErr: nil, wantErrs: []error{errFoo}},
"main: error; close: error": {mainErr: errFoo, closeErr: errBar, wantErrs: []error{errFoo, errBar}},
"main: ok; defer: ok": {mainErr: nil, deferErr: nil, wantErrs: []error{}},
"main: ok; defer: error": {mainErr: nil, deferErr: errBar, wantErrs: []error{errBar}},
"main: error; defer: ok": {mainErr: errFoo, deferErr: nil, wantErrs: []error{errFoo}},
"main: error; defer: error": {mainErr: errFoo, deferErr: errBar, wantErrs: []error{errFoo, errBar}},
}

for name, test := range tests {
t.Run(name, func(t *testing.T) {
gotErr := func() (err error) {
c := errCloser{err: test.closeErr}
defer errorsx.Close(&c, &err)
fn := func() error { return test.deferErr }
defer errorsx.Do(fn, &err)
return test.mainErr
}()
for _, wantErr := range test.wantErrs {
Expand All @@ -92,8 +92,4 @@ type barError struct{}

func (barError) Error() string { return "bar" }

type errCloser struct{ err error }

func (c *errCloser) Close() error { return c.err }

func wrap(err error) error { return fmt.Errorf("%w", err) }
4 changes: 2 additions & 2 deletions example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@ func ExampleAs() {
}
}

func ExampleClose() {
func ExampleDo() {
_ = func() (err error) {
f, err := os.Open("file.txt")
if err != nil {
return err
}
defer errorsx.Close(f, &err)
defer errorsx.Do(f.Close, &err)

return nil
}
Expand Down
Loading