diff --git a/.github/dependabot.yml b/.github/dependabot.yml index f4b45791..c86d2d91 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -74,3 +74,12 @@ updates: commit-message: prefix: "feat" include: "scope" + - package-ecosystem: "gomod" + directory: "/errors" + schedule: + interval: "daily" + labels: + - "dependencies" + commit-message: + prefix: "feat" + include: "scope" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fc3d0601..17a29fb7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -123,3 +123,20 @@ jobs: cache-dependency-path: ./editor.sum - run: go build -v ./... - run: go test -race -v ./... + errors: + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + defaults: + run: + working-directory: ./errors + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version-file: ./errors/go.mod + cache: true + cache-dependency-path: ./errors.sum + - run: go build -v ./... + - run: go test -race -v ./... diff --git a/errors/go.mod b/errors/go.mod new file mode 100644 index 00000000..867a56d7 --- /dev/null +++ b/errors/go.mod @@ -0,0 +1,3 @@ +module github.com/charmbracelet/x/errors + +go 1.21.5 diff --git a/errors/join.go b/errors/join.go new file mode 100644 index 00000000..70745003 --- /dev/null +++ b/errors/join.go @@ -0,0 +1,46 @@ +package errors + +import "strings" + +// Join returns an error that wraps the given errors. +// Any nil error values are discarded. +// Join returns nil if every value in errs is nil. +// The error formats as the concatenation of the strings obtained +// by calling the Error method of each element of errs, with a newline +// between each string. +// +// A non-nil error returned by Join implements the Unwrap() []error method. +// +// This is copied from Go 1.20 errors.Unwrap, with some tuning to avoid using unsafe. +// The main goal is to have this available in older Go versions. +func Join(errs ...error) error { + var nonNil []error + for _, err := range errs { + if err == nil { + continue + } + nonNil = append(nonNil, err) + } + if len(nonNil) == 0 { + return nil + } + return &joinError{ + errs: nonNil, + } +} + +type joinError struct { + errs []error +} + +func (e *joinError) Error() string { + strs := make([]string, 0, len(e.errs)) + for _, err := range e.errs { + strs = append(strs, err.Error()) + } + return strings.Join(strs, "\n") +} + +func (e *joinError) Unwrap() []error { + return e.errs +} diff --git a/errors/join_test.go b/errors/join_test.go new file mode 100644 index 00000000..36eb35b6 --- /dev/null +++ b/errors/join_test.go @@ -0,0 +1,41 @@ +package errors + +import ( + "errors" + "fmt" + "testing" +) + +func TestJoin(t *testing.T) { + t.Run("nil", func(t *testing.T) { + err := Join(nil, nil, nil) + if err != nil { + t.Errorf("expected nil, got %v", err) + } + }) + t.Run("one err", func(t *testing.T) { + expected := fmt.Errorf("fake") + err := Join(nil, expected, nil) + if !errors.Is(err, expected) { + t.Errorf("expected %v, got %v", expected, err) + } + if s := err.Error(); s != expected.Error() { + t.Errorf("expected %s, got %s", expected, err) + } + }) + t.Run("many errs", func(t *testing.T) { + expected1 := fmt.Errorf("fake 1") + expected2 := fmt.Errorf("fake 2") + err := Join(nil, expected1, nil, nil, expected2, nil) + if !errors.Is(err, expected1) { + t.Errorf("expected %v, got %v", expected1, err) + } + if !errors.Is(err, expected2) { + t.Errorf("expected %v, got %v", expected2, err) + } + expectedS := expected1.Error() + "\n" + expected2.Error() + if s := err.Error(); s != expectedS { + t.Errorf("expected %s, got %s", expectedS, err) + } + }) +} diff --git a/go.work b/go.work index 37ece716..07f51c95 100644 --- a/go.work +++ b/go.work @@ -1,7 +1,8 @@ -go 1.21 +go 1.21.5 use ( ./editor + ./errors ./exp/higherorder ./exp/ordered ./exp/slice