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

Initial implementation of required functionality #1

Merged
merged 45 commits into from
Feb 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
55899a2
Draft
e-sumin Oct 9, 2023
3d06197
Add basic unit tests
e-sumin Oct 13, 2023
47a8248
Apply suggestions from code review
e-sumin Oct 24, 2023
bee738a
Add readme
e-sumin Oct 24, 2023
1843239
Refactor WithDetails
e-sumin Nov 13, 2023
bdd48e4
Allow error creation with details
e-sumin Nov 14, 2023
a3d0764
Add PureError type to allow create named errors
e-sumin Nov 14, 2023
ba8ff34
go mod tidy
e-sumin Dec 4, 2023
0223e27
Make an aliases for errors.Is, errors.As, errors.Unwrap
e-sumin Dec 7, 2023
e773da1
Update readme
e-sumin Jan 12, 2024
fbc9cac
Fix typo in documentation
e-sumin Jan 12, 2024
e62bbb1
Remove garbage
e-sumin Jan 12, 2024
749e300
Fix WithCause
e-sumin Jan 12, 2024
e25a933
Test for WithCause
e-sumin Jan 12, 2024
2b0567d
Update readme
e-sumin Jan 12, 2024
280a452
Remove unused garbage
e-sumin Jan 17, 2024
5ff7f11
Remove WithDetails since they are redundant.
e-sumin Jan 17, 2024
ba0d8e9
Remove redundant `wrap` function
e-sumin Jan 17, 2024
076da5a
Fix an error in jsonError
e-sumin Jan 17, 2024
1699058
Simplify PureError creation
e-sumin Jan 17, 2024
789b565
Making interface consistent
e-sumin Jan 17, 2024
a3c2148
Minor internal reorganization
e-sumin Jan 17, 2024
1655dc6
Introduce getUnwrapCheck checker
e-sumin Jan 18, 2024
1bcc3ac
Rename cause checker
e-sumin Jan 18, 2024
3c45313
It is possible to pass details when using WithStack
e-sumin Jan 17, 2024
a897f73
Reformat WithStack comments
e-sumin Jan 18, 2024
fc9a43f
Refactor WithCause using refactored `newError`
e-sumin Jan 18, 2024
9a983ef
Add documentation for WithCause func
e-sumin Jan 18, 2024
06941c0
Make Error unexported
e-sumin Jan 18, 2024
9d18f85
Add notes and tests for specific cases
e-sumin Jan 18, 2024
94ca813
Minor code reordering
e-sumin Jan 18, 2024
2c520d5
WithCause and WithStack should return nil when nil passed
e-sumin Jan 18, 2024
7a91e74
Fix WithStack invocation
e-sumin Jan 19, 2024
3b22e41
Draft 2 (#3)
e-sumin Jan 31, 2024
ffe4803
Rename Pure error to Sentinel error
e-sumin Feb 2, 2024
06606d2
errkit.Append always returns error list
e-sumin Feb 2, 2024
0eb7c44
Fix comments
e-sumin Feb 2, 2024
3d4aa4d
Rremove unnecessary exports
e-sumin Feb 2, 2024
d1fb937
Add test for multiple errors passing from goroutines via channel
e-sumin Feb 5, 2024
b9b2e45
Remove irrelevant comments
e-sumin Feb 5, 2024
4ad955f
Refactor stack dumping
e-sumin Feb 8, 2024
39e1abb
go mod tidy
e-sumin Feb 8, 2024
ca318ee
Add github actions
e-sumin Feb 8, 2024
b5bff9e
Adjust names in tests, so that they satisfy linter rules
e-sumin Feb 8, 2024
711397d
Merge branch 'main' into draft
e-sumin Feb 12, 2024
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
65 changes: 65 additions & 0 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
name: Go

on:
push:
branches:
- '*'
pull_request:
branches:
- '*'

jobs:
build:
strategy:
matrix:
go: ['stable', 'oldstable']
os: ['ubuntu-latest']

runs-on: ${{ matrix.os }}

name: Go ${{ matrix.go }} in ${{ matrix.os }}

steps:
- uses: actions/checkout@v4

- uses: actions/setup-go@v5
name: Install Go
with:
go-version: ${{ matrix.go }}
check-latest: true
cache: true

- name: Go Environment
run: |
go version
go env

- name: Get dependencies
run: go get -v -t -d ./...

- name: Run Fmt
run: go fmt ./...

- name: Run Vet
run: |
go vet -stdmethods=false $(go list ./...)
go mod tidy
if ! test -z "$(git status --porcelain)"; then
echo "Please run 'go mod tidy'"
exit 1
fi

- name: Run Lint
uses: golangci/golangci-lint-action@v3
with:
version: latest
only-new-issues: true
skip-pkg-cache: true

- name: Run Staticcheck
run: |
go install honnef.co/go/tools/cmd/staticcheck@latest
staticcheck ./...

- name: Run Test
run: go test -race -coverpkg=./... -coverprofile=coverage.txt ./...
124 changes: 124 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# Usage:

## Creation
When creating an error, we can use one of the following ways. All errors created this way will capture the current line and filename.

```go
// Creation of error with some message.
someError := errkit.New("Sample of std error")

// Creation of error with some additional details
anotherError := errkit.New("Some error with details", "TextDetail", "Text value", "NumericDetail", 123)
```

Sometimes it could be useful to create predefined errors. Such errors will not capture the stack.
```go
var (
ErrNotFound := errkit.NewSentinelErr("Not found")
ErrAlreadyExists := errkit.NewSentinelErr("Already exists")
)

func Foo() error {
...
return ErrNotFound
}
```

## Wrapping

### Adding stack trace
If you are interested in adding information about the line and filename where the sentinel error happened, you can do the following:
```go
func Foo() error {
...
err := errkit.WithStack(ErrNotFound)
return err
}

func Bar() error {
err := Foo()
if err != nil && errkit.Is(err, ErrNotFound) {
fmt.Println("Resource not found, do nothing")
return nil
}
...
}
```

### Adding error cause information
Sometimes you might be interested in returning a sentinel error, but also add some cause error to it, in such cases you can do the following:
```go
func FetchSomething(ID string) error {
err := doSomething() // Here we have an error
if err != nil { // At this step we decide that in such a case we'd like to say that the resource is not found
return errkit.WithCause(ErrNotFound, err)
}

return nil
}

func FooBar() error {
err := FetchSomething()
if err == nil {
return nil
}

if errkit.Is(err, ErrNotFound) {
return nil // Not found is an expected scenario here, do nothing
}

// Errors other than NotFound should be returned as is
return err
}
```

### Wrapping an error with a high-level message
Sometimes you might want to add some high-level information to an error before passing it up to the invoker.
```go
func LoadProfile() error {
err := makeAnApiCall()
if err != nil {
return errkit.Wrap(err, "Unable to load profile")
}
return nil
}

```

### Unwrapping errors
If needed, you can always get the wrapped error using the standard errors.Unwrap method, it also has an alias errkit.Unwrap
```go
var (
ErrSomething = errkit.NewSentinelErr("Some error")
)

wrappedError := errkit.Wrap(ErrSomething, "Wrapped error")

err := errors.Unwrap(wrappedError)
if err != ErrSomething {
return errors.New("Unable to unwrap error cause")
}
```

### Matching an errors
You can use standard errors matching methods like errors.Is and errors.As, they also have aliases errkit.Is and errkit.As
```go
// testErrorType which implements std error interface
type testErrorType struct {
message string
}

var (
ErrTestType = newTestError("Sample error of custom type")
)

wrappedTestError := errkit.Wrap(ErrTestType, "Wrapped TEST error")
if !errors.Is(wrappedTestError, ErrTestType) {
return errors.New("error is not implementing requested type")
}

var asErr *testErrorType
if !errors.As(origErr, &asErr) {
return errors.New("unable to cast error to its cause")
}
```
50 changes: 50 additions & 0 deletions error_details.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package errkit

import (
"fmt"
)

const (
noVal = "NOVAL"
badKey = "BADKEY"
)

type ErrorDetails map[string]any

// ToErrorDetails accepts either an even size array which contains pais of key/value
// or array of one element of ErrorDetails type.
// Result of function is an ErrorDetails
func ToErrorDetails(details []any) ErrorDetails {
if len(details) == 0 {
return nil
}

if len(details) == 1 {
if dp, ok := details[0].(ErrorDetails); ok {
// Actually we have ErrorDetails on input, so just make a copy
errorDetails := make(ErrorDetails, len(dp))
for k, v := range dp {
errorDetails[k] = v
}
return errorDetails
}
}

// It might happen that odd number of elements will be passed, trying our best to handle this case
if len(details)%2 != 0 {
details = append(details, noVal)
}

errorDetails := make(ErrorDetails, len(details)/2)
for i := 0; i < len(details); i += 2 {
name := details[i]
nameStr, ok := name.(string)
if !ok {
nameStr = fmt.Sprintf("%s:(%v)", badKey, name)
}

errorDetails[nameStr] = details[i+1]
}

return errorDetails
}
51 changes: 51 additions & 0 deletions error_details_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package errkit_test

import (
"testing"

qt "github.com/frankban/quicktest"
"github.com/kanisterio/errkit"
)

type testStruct struct {
Foo string
Bar int
Baz *testStruct
}

func TestToErrorDetails(t *testing.T) {
cases := []struct {
testName string
args []any
expected errkit.ErrorDetails
}{
{
testName: "ErrorDetails as an argument",
args: []any{errkit.ErrorDetails{"key": "value"}},
expected: errkit.ErrorDetails{"key": "value"},
},
{
testName: "Sequence of keys and values of any type",
args: []any{"string_key", "string value", "int key", 123, "struct key", testStruct{Foo: "aaa", Bar: 123, Baz: &testStruct{Foo: "bbb", Bar: 234}}},
expected: errkit.ErrorDetails{"string_key": "string value", "int key": 123, "struct key": testStruct{Foo: "aaa", Bar: 123, Baz: &testStruct{Foo: "bbb", Bar: 234}}},
},
{
testName: "Odd number of arguments",
args: []any{"key_1", 1, "key_2"},
expected: errkit.ErrorDetails{"key_1": 1, "key_2": "NOVAL"},
},
{
testName: "Argument which is supposed to be a key is not a string",
args: []any{123, 456},
expected: errkit.ErrorDetails{"BADKEY:(123)": 456},
},
}

for _, tc := range cases {
t.Run(tc.testName, func(t *testing.T) {
c := qt.New(t)
result := errkit.ToErrorDetails(tc.args)
c.Assert(result, qt.DeepEquals, tc.expected)
})
}
}
103 changes: 103 additions & 0 deletions error_list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package errkit

import (
"bytes"
"encoding/json"
"errors"
"fmt"
"strconv"
)

type ErrorList []error

var _ error = (ErrorList)(nil)
var _ json.Marshaler = (ErrorList)(nil)

func (e ErrorList) String() string {
sep := ""
var buf bytes.Buffer
buf.WriteRune('[')
for _, err := range e {
buf.WriteString(sep)
sep = ","
buf.WriteString(strconv.Quote(err.Error()))
}
buf.WriteRune(']')
return buf.String()
}

func (e ErrorList) Error() string {
return e.String()
}

// As allows error.As to work against any error in the list.
func (e ErrorList) As(target any) bool {
for _, err := range e {
if errors.As(err, target) {
return true
}
}
return false
}

// Is allows error.Is to work against any error in the list.
func (e ErrorList) Is(target error) bool {
for _, err := range e {
if errors.Is(err, target) {
return true
}
}
return false
}

func (e ErrorList) MarshalJSON() ([]byte, error) {
var je struct {
Message string `json:"message"`
Errors []json.RawMessage `json:"errors"`
}

switch len(e) {
case 0:
// no errors
return []byte("null"), nil
case 1:
// this is unlikely to happen as kerrors.Append won't allow having just a single error on the list
je.Message = "1 error has occurred"
default:
je.Message = fmt.Sprintf("%d errors have occurred", len(e))
}

je.Errors = make([]json.RawMessage, 0, len(e))
for i := range e {
raw, err := json.Marshal(jsonMarshable(e[i]))
if err != nil {
return nil, err
}

je.Errors = append(je.Errors, raw)
}

return json.Marshal(je)
}

// Append creates a new combined error from err1, err2. If either error is nil,
// then the other error is returned.
func Append(err1, err2 error) error {
if err1 == nil {
return ErrorList{err2}
}
if err2 == nil {
return ErrorList{err1}
}
el1, ok1 := err1.(ErrorList)
el2, ok2 := err2.(ErrorList)
switch {
case ok1 && ok2:
return append(el1, el2...)
case ok1:
return append(el1, err2)
case ok2:
return append(el2, err1)
}
return ErrorList{err1, err2}
}
Loading
Loading