Skip to content

Commit

Permalink
Merge pull request #5 from zignd/add-is-err-composition
Browse files Browse the repository at this point in the history
Add IsErrComposition to help check if a custom error type is a composition of Err
  • Loading branch information
zignd authored Jul 24, 2024
2 parents 42b9692 + 41b11e8 commit 92c8c61
Show file tree
Hide file tree
Showing 4 changed files with 218 additions and 119 deletions.
30 changes: 29 additions & 1 deletion errors.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package errors

import "fmt"
import (
"fmt"
"reflect"
)

type Data map[string]any

Expand Down Expand Up @@ -101,3 +104,28 @@ func WithCause(err error, cause error) error {
return err
}
}

// IsErrComposition returns true if the provided error is a composition of Err or *Err.
func IsErrComposition(err error) bool {
typeOfErr := reflect.TypeOf(err)

if typeOfErr.Kind() == reflect.Pointer {
typeOfErr = typeOfErr.Elem()
}

if typeOfErr.Kind() != reflect.Struct {
return false
}

for i := 0; i < typeOfErr.NumField(); i++ {
if typeOfErr.Field(i).Type == reflect.TypeOf(Err{}) {
return true
}

if typeOfErr.Field(i).Type == reflect.TypeOf((*Err)(nil)) {
return true
}
}

return false
}
235 changes: 143 additions & 92 deletions errors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,133 +8,184 @@ import (
"testing"
)

// CustomError is a custom error type composed with Err.
type CustomError struct {
*Err
}

// NewCustomError returns a new CustomError and adds a stack trace.
func NewCustomError(message string) error {
customError := CustomError{Err: &Err{Message: message}}
WithStack(customError.Err)
return customError
}

// CustomError2 is a custom error type composed with Err.
type CustomError2 struct {
*Err
}

// NewCustom2Error returns a new CustomError2 and adds a cause to the error.
func NewCustom2Error(message string, cause error) error {
customError2 := CustomError2{Err: &Err{Message: message}}
WithCause(customError2.Err, cause)
return customError2
}

// customError3 is a custom error type not composed with Err.
type customError3 struct{}

func (e customError3) Error() string {
return "this is a custom error type"
}

func TestNew(t *testing.T) {
msg := "error message"
if got := New(msg).Error(); got != msg {
t.Errorf(`wrong error message, got "%v", expected "%v"`, got, msg)
return
}
t.Run("when New is provided with a message, it should create a new error with the message", func(t *testing.T) {
msg := "error message"
if got := New(msg).Error(); got != msg {
t.Errorf(`wrong error message, got "%v", expected "%v"`, got, msg)
return
}
})
}

func TestErrorc(t *testing.T) {
msg := "error message"
data := Data{
"id": 1,
"description": "fool",
}

err := Errord(data, msg)
if got := err.Error(); got != msg {
t.Errorf(`wrong error message, got "%v", expected "%v"`, got, msg)
return
}

if e := err.(*Err); !reflect.DeepEqual(e.Data, data) {
t.Errorf(`wrong data, got %+v, expected %+v`, e.Data, data)
return
}
func TestErrord(t *testing.T) {
t.Run("when Errord is provided with a message and data, it should create a new error with the message and data", func(t *testing.T) {
msg := "error message"
data := Data{
"id": 1,
"description": "fool",
}

err := Errord(data, msg)
if got := err.Error(); got != msg {
t.Errorf(`wrong error message, got "%v", expected "%v"`, got, msg)
return
}

if e := err.(*Err); !reflect.DeepEqual(e.Data, data) {
t.Errorf(`wrong data, got "%+v", expected "%+v"`, e.Data, data)
return
}
})
}

func TestWrap(t *testing.T) {
msg1 := "error message 1"
err1 := New(msg1)
msg2 := "error message 2"
err2 := Wrap(err1, msg2)
msg3 := "error message 3"
err3 := Wrap(err2, msg3)
got := err3.Error()
expected := fmt.Sprintf("%s: %s: %s", msg3, msg2, msg1)
if got != expected {
t.Errorf(`wrong error message, got "%s", expected "%s"`, got, expected)
return
}
t.Run("when Wrap is provided with an error and a message, it should create a new error with the message and the provided error as the cause", func(t *testing.T) {
msg1 := "error message 1"
err1 := New(msg1)
msg2 := "error message 2"
err2 := Wrap(err1, msg2)
msg3 := "error message 3"
err3 := Wrap(err2, msg3)
got := err3.Error()
expected := fmt.Sprintf("%s: %s: %s", msg3, msg2, msg1)
if got != expected {
t.Errorf(`wrong error message, got "%s", expected "%s"`, got, expected)
return
}
})
}

func TestWrapc(t *testing.T) {
msg1 := "error message 1"
err1 := errors.New(msg1)

msg2 := "error message 2"
data2 := Data{
"id": 2,
"description": "bar",
}
err2 := Wrapd(err1, data2, msg2)

msg3 := "error message 3"
data3 := Data{
"id": 3,
"description": "spam",
}
err3 := Wrapd(err2, data3, msg3)

msg4 := "error message 4"
data4 := Data{
"id": 4,
"description": "spam",
}
err4 := Wrapd(err3, data4, msg4)

got := err4.Error()
expected := fmt.Sprintf("%s: %s: %s: %s", msg4, msg3, msg2, msg1)
if got != expected {
t.Errorf(`wrong error message, got "%s", expected "%s"`, got, expected)
return
}
}
t.Run("when Wrapd is provided with an error and data, it should add the data to the error", func(t *testing.T) {
msg1 := "error message 1"
err1 := errors.New(msg1)

msg2 := "error message 2"
data2 := Data{
"id": 2,
"description": "bar",
}
err2 := Wrapd(err1, data2, msg2)

// CustomError is a custom error type composed with Err.
type CustomError struct {
*Err
}
msg3 := "error message 3"
data3 := Data{
"id": 3,
"description": "spam",
}
err3 := Wrapd(err2, data3, msg3)

// NewCustomError returns a new CustomError and adds a stack trace.
func NewCustomError(message string) error {
customError := CustomError{Err: &Err{Message: message}}
WithStack(customError.Err)
return customError
msg4 := "error message 4"
data4 := Data{
"id": 4,
"description": "spam",
}
err4 := Wrapd(err3, data4, msg4)

got := err4.Error()
expected := fmt.Sprintf("%s: %s: %s: %s", msg4, msg3, msg2, msg1)
if got != expected {
t.Errorf(`wrong error message, got "%s", expected "%s"`, got, expected)
return
}

if e := err4.(*Err); !reflect.DeepEqual(e.Data, data4) {
t.Errorf(`wrong data, got "%+v", expected "%+v"`, e.Data, data4)
return
}

if e := err3.(*Err); !reflect.DeepEqual(e.Data, data3) {
t.Errorf(`wrong data, got "%+v", expected "%+v"`, e.Data, data3)
return
}

if e := err2.(*Err); !reflect.DeepEqual(e.Data, data2) {
t.Errorf(`wrong data, got "%+v", expected "%+v"`, e.Data, data2)
return
}
})
}

func TestWithStack(t *testing.T) {
t.Run("when WithStack is provided with an error of type Err, it should add a stack trace to the error", func(t *testing.T) {
err := NewCustomError("this is a custom error type with stack")

if err.(CustomError).Stack == nil {
t.Errorf(`expected stack to be not nil, got nil`)
t.Fatal("expected stack to be not nil, got nil")
return
}

outputStr := fmt.Sprintf("%+v", err)
if !strings.Contains(outputStr, "message:") {
t.Errorf(`expected "message:" to be in the output string, got %v`, outputStr)
return
t.Errorf(`expected "message:" to be in the output string, got "%v"`, outputStr)
}
if !strings.Contains(outputStr, "stack:") {
t.Errorf(`expected "stack:" to be in the output string, got %v`, outputStr)
return
t.Errorf(`expected "stack:" to be in the output string, got "%v"`, outputStr)
}
})
}

// CustomError2 is a custom error type composed with Err.
type CustomError2 struct {
*Err
}

// NewCustom2Error returns a new CustomError2 and adds a cause to the error.
func NewCustom2Error(message string, cause error) error {
customError2 := CustomError2{Err: &Err{Message: message}}
WithCause(customError2.Err, cause)
return customError2
}

func TestWithCause(t *testing.T) {
t.Run("when WithCause is provided with an error and a cause, it should add the cause to the error", func(t *testing.T) {
causeErr := New("inner error")
err := NewCustom2Error("outer error", causeErr)

if err.(CustomError2).Cause != causeErr {
t.Errorf(`expected cause to be %v, got %v`, causeErr, err.(CustomError2).Cause)
t.Errorf(`expected cause to be "%v", got "%v"`, causeErr, err.(CustomError2).Cause)
}
})
}

func TestIsErrComposition(t *testing.T) {
t.Run("when a custom error type is composed with *Err, it should return true", func(t *testing.T) {
err := NewCustomError("this is a custom error type with stack")
if !IsErrComposition(err) {
t.Errorf("expected IsErrComposition to return true, got false")
}
})

t.Run("when an error type is Pointer but the element type is a struct not composed with *Err, it should return false", func(t *testing.T) {
err := errors.New("this is a regular error")
if IsErrComposition(err) {
t.Errorf("expected IsErrComposition to return false, got true")
}
})

t.Run("when a custom error type is not composed with *Err, it should return false", func(t *testing.T) {
err := customError3{}
if IsErrComposition(err) {
t.Errorf("expected IsErrComposition to return false, got true")
}
})
}
26 changes: 21 additions & 5 deletions go113_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ type customErr struct {
func (c customErr) Error() string { return c.msg }

func TestGo113Compatibility(t *testing.T) {
t.Run("Wrap should be able to return an error compatible with the standard library Is", func(t *testing.T) {
t.Run("when Wrap is used to wrap a standard error, it should return an error compatible with the standard library Is", func(t *testing.T) {
// First we create an error using the standard library
err := stderrors.New("error that gets wrapped")

Expand All @@ -25,11 +25,12 @@ func TestGo113Compatibility(t *testing.T) {

// Finally we check if the standard library Is function can handle our wrapped error
if !stderrors.Is(wrapped, err) {
t.Errorf("Wrap does not support Go 1.13 error chains")
t.Errorf("our Wrap does not support Go 1.13 error chains")
}
})

t.Run("Is should be able to handle errors created and wrapped using the standard Go features", func(t *testing.T) {
// Is should be able to handle errors created and wrapped using the standard Go features
t.Run("when Is is used to check if an error is a certain error, it should behave just like the equivalent Is function in the standard library", func(t *testing.T) {
// First we create an error using the standard Go features
err := customErr{msg: "test message"}
wrapped := fmt.Errorf("wrap it: %w", err)
Expand All @@ -38,9 +39,14 @@ func TestGo113Compatibility(t *testing.T) {
if !Is(wrapped, err) {
t.Error("Is failed")
}

// Finally just to make sure, we check if the standard library Is function can handle it
if !stderrors.Is(wrapped, err) {
t.Error("stderrors.Is failed")
}
})

t.Run("As should be able to handle errors created and wrapped using the standard Go features", func(t *testing.T) {
t.Run("when As is used to check if an error is a certain error, it should behave just like the equivalent As function in the standard library", func(t *testing.T) {
// First we create an error using the standard Go features
err := customErr{msg: "test message"}
wrapped := fmt.Errorf("wrap it: %w", err)
Expand All @@ -50,14 +56,24 @@ func TestGo113Compatibility(t *testing.T) {
if !As(wrapped, target) {
t.Error("As failed")
}

// Finally just to make sure, we check if the standard library As function can handle it
if !stderrors.As(wrapped, target) {
t.Error("stderrors.As failed")
}
})

t.Run("Unwrap should be able to handle errors created and wrapped using the standard Go features", func(t *testing.T) {
// Unwrap should be able to handle errors created and wrapped using the standard Go features
t.Run("when Unwrap is used to unwrap an error, it should behave just like the equivalent Unwrap function in the standard library", func(t *testing.T) {
err := customErr{msg: "test message"}
wrapped := fmt.Errorf("wrap it: %w", err)

if unwrappedErr := Unwrap(wrapped); !reflect.DeepEqual(unwrappedErr, err) {
t.Error("Unwrap failed")
}

if unwrappedErr := stderrors.Unwrap(wrapped); !reflect.DeepEqual(unwrappedErr, err) {
t.Error("stderrors.Unwrap failed")
}
})
}
Loading

0 comments on commit 92c8c61

Please sign in to comment.