-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Initial implementation of required functionality (#1)
- Loading branch information
Showing
11 changed files
with
1,262 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 ./... |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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} | ||
} |
Oops, something went wrong.