diff --git a/.gitignore b/.gitignore index 6f72f89..eb74f5a 100644 --- a/.gitignore +++ b/.gitignore @@ -15,7 +15,7 @@ *.out # Dependency directories (remove the comment below to include it) -# vendor/ +vendor/ # Go workspace file go.work @@ -23,3 +23,5 @@ go.work.sum # env file .env +# Coverage file +.cover diff --git a/error_test.go b/error_test.go new file mode 100644 index 0000000..6ba4792 --- /dev/null +++ b/error_test.go @@ -0,0 +1,23 @@ +package errors + +import "testing" + +func TestErrorError(t *testing.T) { + rootErr := New("root error") + + t.Run("Unwrap", func(t *testing.T) { + var err error = Error{err: rootErr} + if unwrapped := err.(Error).Unwrap(); unwrapped != rootErr { + t.Errorf("expected %v, got %v", rootErr, unwrapped) + } + }) + + t.Run("Error", func(t *testing.T) { + var err error = Error{err: rootErr} + expected := "root error" + if err.Error() != expected { + t.Errorf("expected %q, got %q", expected, err.Error()) + } + }) + +} diff --git a/formatter.go b/formatter.go index 25220f5..33f14cf 100644 --- a/formatter.go +++ b/formatter.go @@ -111,7 +111,7 @@ func writeKV(sb *strings.Builder, kvs []KeyValuer) { } sb.WriteString(stringify(kv.Key())) sb.WriteString(": ") - sb.WriteString(kv.String()) + sb.WriteString(stringify(kv.Value())) shouldAddComma = true } diff --git a/formatter_test.go b/formatter_test.go index d714d62..5872044 100644 --- a/formatter_test.go +++ b/formatter_test.go @@ -6,6 +6,25 @@ import ( "github.com/arquivei/errors" ) +func TestFormatter(t *testing.T) { + formatter := errors.Formatter(func(err error) string { + return "formatted error: " + err.Error() + }) + + if formatter == nil { + t.Error("expected non-nil formatter") + } + if formatter.String() != "" { + t.Errorf("expected '', got '%s'", formatter.String()) + } + if formatter.Key() == nil { + t.Error("expected non-nil key") + } + if formatter.Value() == nil { + t.Error("expected non-nil value") + } +} + func TestGetFormatter(t *testing.T) { err := errors.New("some error") if errors.GetFormatter(err) == nil { @@ -35,3 +54,33 @@ func TestGetFormatter(t *testing.T) { t.Error("expected custom formatter, got", err.Error()) } } + +func TestRootErrorFormatter(t *testing.T) { + rootErr := errors.New("root error") + err := errors.With( + rootErr, + errors.RootErrorFormatter, + errors.SeverityInput, + errors.Code("BAD_REQUEST"), + errors.KV("key1", "value1"), + ) + + if err.Error() != "root error" { + t.Errorf("expected 'root error', got '%s'", err.Error()) + } +} +func TestRootErrorKVFormatter(t *testing.T) { + rootErr := errors.New("root error") + err := errors.With( + rootErr, + errors.RootErrorKVFormatter, + errors.SeverityInput, + errors.Code("BAD_REQUEST"), + errors.KV("key1", "value1"), + ) + + expected := "root error {key1: value1}" + if err.Error() != expected { + t.Errorf("expected '%s', got '%s'", expected, err.Error()) + } +} diff --git a/kv.go b/kv.go index b109dc4..f750ff9 100644 --- a/kv.go +++ b/kv.go @@ -10,7 +10,6 @@ import ( type KeyValuer interface { Key() any Value() any - String() string } type KeyValue struct { @@ -28,10 +27,6 @@ func (kv KeyValue) Value() any { return kv.value } -func (kv KeyValue) String() string { - return stringify(kv.value) -} - // KV is a constructor for KeyValuer types. func KV(key any, value any) KeyValuer { return KeyValue{ @@ -46,7 +41,7 @@ func KV(key any, value any) KeyValuer { // NOTE: Extracted from the context package. func stringify(v any) string { switch s := v.(type) { - case stringer: + case fmt.Stringer: return s.String() case string: return s @@ -57,7 +52,3 @@ func stringify(v any) string { } return fmt.Sprintf("%v", v) } - -type stringer interface { - String() string -} diff --git a/kv_test.go b/kv_test.go new file mode 100644 index 0000000..52815d3 --- /dev/null +++ b/kv_test.go @@ -0,0 +1,33 @@ +package errors + +import "testing" + +type stringer struct{} + +func (s stringer) String() string { + return "stringer" +} + +func Test_stringify(t *testing.T) { + tests := []struct { + name string + input any + expected string + }{ + {"nil", nil, ""}, + {"string", "test", "test"}, + {"int", 42, "42"}, + {"float64", 3.14, "3.14"}, + {"bool", true, "true"}, + {"stringer", stringer{}, "stringer"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := stringify(tt.input) + if result != tt.expected { + t.Errorf("expected %q, got %q", tt.expected, result) + } + }) + } +} diff --git a/std.go b/std.go index 3c29e87..ad1e796 100644 --- a/std.go +++ b/std.go @@ -7,26 +7,38 @@ import ( "fmt" ) +// Unwrap returns the next error in the chain, if any. +// This calls the standard library's errors.Unwrap function func Unwrap(err error) error { return errors.Unwrap(err) } +// Is checks if the target error is present in the error chain. +// This calls the standard library's errors.Is function. func Is(err, target error) bool { return errors.Is(err, target) } +// As checks if the error can be cast to the target type. +// This calls the standard library's errors.As function. func As(err error, target any) bool { return errors.As(err, target) } +// New creates a new error with the given string message. +// This calls the standard library's errors.New function. func New(str string) error { return errors.New(str) } +// Errorf formats an error message according to a format specifier and returns it as an error. +// This calls the standard library's fmt.Errorf function. func Errorf(format string, args ...any) error { return fmt.Errorf(format, args...) } +// Join combines multiple errors into a single error. +// This calls the standard library's errors.Join function. func Join(errs ...error) error { return errors.Join(errs...) } diff --git a/std_test.go b/std_test.go new file mode 100644 index 0000000..4b5a2b6 --- /dev/null +++ b/std_test.go @@ -0,0 +1,75 @@ +package errors + +import "testing" + +func TestUnwrap(t *testing.T) { + rootErr := New("root error") + wrappedErr := With(rootErr) + + if _, ok := wrappedErr.(Error); !ok { + t.Fatal("expected wrappedErr to be of type Error") + } + + if unwrapped := Unwrap(wrappedErr); unwrapped != rootErr { + t.Errorf("expected %v, got %v", rootErr, unwrapped) + } +} + +func TestIs(t *testing.T) { + rootErr := New("root error") + err := With(rootErr, KV("key", "value")) + err = With(err, KV("key2", "value2")) + + if _, ok := err.(Error); !ok { + t.Fatal("expected wrappedErr to be of type Error") + } + + if !Is(err, rootErr) { + t.Errorf("expected error to match rootErr, got %v", err) + } +} + +func TestAs(t *testing.T) { + rootErr := New("root error") + err := With(rootErr, KV("key", "value")) + + var e Error + if !As(err, &e) { + t.Fatal("expected error to be of type Error") + } +} + +func TestNew(t *testing.T) { + err := New("test error") + if err == nil { + t.Fatal("expected non-nil error, got nil") + } + + if err.Error() != "test error" { + t.Errorf("expected 'test error', got %s", err.Error()) + } +} + +func TestErrorf(t *testing.T) { + err := Errorf("test error: %s", "details") + if err == nil { + t.Fatal("expected non-nil error, got nil") + } + + if err.Error() != "test error: details" { + t.Errorf("expected 'test error: details', got %s", err.Error()) + } +} + +func TestJoin(t *testing.T) { + err1 := New("first error") + err2 := New("second error") + + err := Join(err1, err2) + if err == nil { + t.Fatal("expected non-nil error, got nil") + } + if err.Error() != "first error\nsecond error" { + t.Errorf("expected 'first error\nsecond error', got %s", err.Error()) + } +} diff --git a/value.go b/value.go index bcffc6b..84857bb 100644 --- a/value.go +++ b/value.go @@ -9,7 +9,7 @@ import ( func Value(err error, key any) any { for ; err != nil; err = errors.Unwrap(err) { if e, ok := err.(Error); ok { - if e.keyval.Key() == key { + if e.keyval != nil && e.keyval.Key() == key { return e.keyval.Value() } } @@ -33,7 +33,7 @@ func Values(err error, key any) []any { var e Error for ; err != nil; err = errors.Unwrap(err) { if errors.As(err, &e) { - if e.keyval.Key() == key { + if e.keyval != nil && e.keyval.Key() == key { values = append(values, e.keyval.Value()) } } @@ -50,13 +50,11 @@ func ValuesT[T any](err error, key any) []T { if len(values) == 0 { return nil } - tValues := make([]T, 0, len(values)) for _, v := range values { if v == nil { continue } - if t, ok := v.(T); ok { tValues = append(tValues, t) } @@ -74,6 +72,9 @@ func ValueAllSlice(err error) []KeyValuer { for ; err != nil; err = errors.Unwrap(err) { if e, ok := err.(Error); ok { + if e.keyval == nil { + continue + } if isBuiltInKeyValuer(e.keyval.Key()) { continue } @@ -93,6 +94,9 @@ func ValuesMapOf(err error, keyType any) map[any][]any { m := make(map[any][]any) for ; err != nil; err = errors.Unwrap(err) { if e, ok := err.(Error); ok { + if e.keyval == nil { + continue + } if reflect.TypeOf(e.keyval.Key()) == reflect.TypeOf(keyType) { m[e.keyval.Key()] = append(m[e.keyval.Key()], e.keyval.Value()) } @@ -109,6 +113,9 @@ func ValueMap(err error) map[any]any { m := make(map[any]any) for ; err != nil; err = errors.Unwrap(err) { if e, ok := err.(Error); ok { + if e.keyval == nil { + continue + } if isBuiltInKeyValuer(e.keyval.Key()) { continue } @@ -127,6 +134,9 @@ func ValueMapOf(err error, keyType any) map[any]any { m := make(map[any]any) for ; err != nil; err = errors.Unwrap(err) { if e, ok := err.(Error); ok { + if e.keyval == nil { + continue + } if reflect.TypeOf(e.keyval.Key()) == reflect.TypeOf(keyType) { if _, ok := m[e.keyval.Key()]; !ok { m[e.keyval.Key()] = e.keyval.Value() diff --git a/value_test.go b/value_test.go new file mode 100644 index 0000000..0ecef2b --- /dev/null +++ b/value_test.go @@ -0,0 +1,218 @@ +package errors + +import ( + "reflect" + "testing" +) + +func TestValue(t *testing.T) { + err := New("root error") + + key := "testKey" + + // Test Value with no key-value pairs + if Value(err, key) != nil { + t.Error("expected nil value for root error") + } + + // Test Value with a key-value pair + expectedValue := "testValue" + err = With(err, KV(key, "testValue")) + if v := Value(err, key); v != expectedValue { + t.Errorf("expected value %v for key %v, got %v", expectedValue, key, v) + } + + // Test Value changing the value for the same key + expectedValue = "anotherValue" + err = With(err, KV(key, expectedValue)) + if v := Value(err, key); v != expectedValue { + t.Errorf("expected value %v for key %v, got %v", expectedValue, key, v) + } +} + +func TestValueT(t *testing.T) { + err := With( + New("root error"), + KV("int", 42), + KV("string", "testValue"), + KV("bool", true), + KV("slice", []string{"a", "b", "c"}), + KV("map", map[string]int{"key1": 1, "key2": 2}), + KV("nil", nil), + KV("empty", ""), + KV("struct", struct{}{}), + ) + + if v := ValueT[int](err, "int"); v != 42 { + t.Errorf("expected int value 42, got %d", v) + } + if v := ValueT[string](err, "string"); v != "testValue" { + t.Errorf("expected string value 'testValue', got %s", v) + } + if v := ValueT[bool](err, "bool"); !v { + t.Errorf("expected bool value true, got %t", v) + } + if v := ValueT[[]string](err, "slice"); !reflect.DeepEqual(v, []string{"a", "b", "c"}) { + t.Errorf("expected slice value ['a', 'b', 'c'], got %v", v) + } + if v := ValueT[map[string]int](err, "map"); !reflect.DeepEqual(v, map[string]int{"key1": 1, "key2": 2}) { + t.Errorf("expected map value {'key1': 1, 'key2': 2}, got %v", v) + } + if v := ValueT[any](err, "nil"); v != nil { + t.Error("expected nil value for key 'nil'") + } + if v := ValueT[string](err, "empty"); v != "" { + t.Error("expected empty string value for key 'empty'") + } + if v := ValueT[struct{}](err, "struct"); !reflect.DeepEqual(v, struct{}{}) { + t.Error("expected empty struct value for key 'struct'") + } +} + +func TestValues(t *testing.T) { + err := With( + New("root error"), + KV("key", "value1"), + KV("key", "value2"), + KV("key", "value3"), + ) + values := Values(err, "key") + if !reflect.DeepEqual(values, []any{"value3", "value2", "value1"}) { + t.Errorf("expected values ['value3', 'value2', 'value1'], got %v", values) + } +} + +func TestValuesT(t *testing.T) { + emprtyErr := Error{err: New("empty error")} + if values := ValuesT[string](emprtyErr, "key"); values != nil { + t.Error("expected nil values for empty error") + } + + rootErr := New("root error") + // Test ValuesT with no key-value pairs + values := ValuesT[string](rootErr, "key") + if values != nil { + t.Error("expected nil values for root error") + } + + // Test ValuesT with a key-value pair + err := With( + rootErr, + KV("key", "value1"), + KV("key", "value2"), + KV("key", nil), // This should be ignored + KV("key", "value3"), + ) + values = ValuesT[string](err, "key") + if !reflect.DeepEqual(values, []string{"value3", "value2", "value1"}) { + t.Errorf("expected values ['value3', 'value2', 'value1'], got %v", values) + } + + // Test ValuesT with mixed types + err = With( + rootErr, + KV("key", "stringValue"), + KV("key", 42), + KV("key", true), + KV("key", 123), + ) + values2 := ValuesT[int](err, "key") + if !reflect.DeepEqual(values2, []int{123, 42}) { + t.Errorf("expected values [123, 42], got %v", values2) + } +} + +func TestValuesMapOf(t *testing.T) { + emprtyErr := Error{err: New("empty error")} + if valuesMap := ValuesMapOf(emprtyErr, "key"); len(valuesMap) != 0 { + t.Error("expected empty map for empty error") + } + + rootErr := New("root error") + // Test ValuesMapOf with no key-value pairs + valuesMap := ValuesMapOf(rootErr, "key") + if len(valuesMap) != 0 { + t.Errorf("expected empty map for root error: %v", valuesMap) + } + + // Test ValuesMapOf with a key-value pair + err := With( + rootErr, + KV("key1", "value1.1"), + KV("key1", "value1.2"), + KV("key2", "value2"), + KV("key3", "value3"), + ) + valuesMap = ValuesMapOf(err, "key") + expectedMap := map[any][]any{ + "key1": {"value1.2", "value1.1"}, + "key2": {"value2"}, + "key3": {"value3"}, + } + if !reflect.DeepEqual(expectedMap, valuesMap) { + t.Errorf("expected values map %v, got %v", expectedMap, valuesMap) + } +} + +func TestValueMap(t *testing.T) { + var err error = Error{err: New("empty error")} + if valueMap := ValueMap(err); len(valueMap) != 0 { + t.Error("expected empty map for empty error") + } + + err = New("root error") + err = With( + err, + KV("key1", "value1-wont-be-included"), + KV("key1", "value1"), + KV("key2", "value2"), + KV("key3", "value3"), + ) + + valueMap := ValueMap(err) + if len(valueMap) != 3 { + t.Errorf("expected map with 3 key-value pairs, got %d", len(valueMap)) + } + expectedMap := map[any]any{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + } + for k, v := range expectedMap { + if value, exists := valueMap[k]; !exists || value != v { + t.Errorf("expected key %v with value %v, got %v", k, v, valueMap[k]) + } + } +} + +func TestValueMapOf(t *testing.T) { + emprtyErr := Error{err: New("empty error")} + if valuesMap := ValueMapOf(emprtyErr, "key"); len(valuesMap) != 0 { + t.Error("expected empty map for empty error") + } + + rootErr := New("root error") + // Test ValuesMapOf with no key-value pairs + valuesMap := ValueMapOf(rootErr, "key") + if len(valuesMap) != 0 { + t.Errorf("expected empty map for root error: %v", valuesMap) + } + + type anotherKeyType int + // Test ValuesMapOf with a key-value pair + err := With( + rootErr, + KV("key1", "value1-wont-be-included"), + KV("key1", "value1"), + KV("key2", "value2"), + KV(anotherKeyType(0), "value3"), + ) + valuesMap = ValueMapOf(err, "key") + expectedMap := map[any]any{ + "key1": "value1", + "key2": "value2", + } + if !reflect.DeepEqual(expectedMap, valuesMap) { + t.Errorf("expected values map %v, got %v", expectedMap, valuesMap) + } +} diff --git a/with.go b/with.go index b27ce2c..094595b 100644 --- a/with.go +++ b/with.go @@ -16,6 +16,9 @@ var ( // for anonymous functions. // If set to true, the Op will include the line number where the anonymous function was defined. VerboseOpOnAnonymousFunctions = true + + // ErrKeyNotComparable defines an error that is returned when a key in With is not comparable. + ErrKeyNotComparable = New("key is not comparable") ) // With adds key-value pairs to an error, allowing for additional context. @@ -28,7 +31,7 @@ func With(err error, keyvalues ...KeyValuer) error { for _, keyval := range keyvalues { if !reflect.TypeOf(keyval.Key()).Comparable() { - panic("key is not comparable") + panic(ErrKeyNotComparable) } err = Error{err: err, keyval: keyval} if keyval.Key() == (opKey{}) { @@ -58,15 +61,40 @@ func getWithCaller() string { } funcName := runtime.FuncForPC(pc).Name() + funcName = discardPackagePath(funcName) + if VerboseOpOnAnonymousFunctions && isAnonymousFunction(funcName) { - funcName += " (line " + strconv.Itoa(line) + ")" + return funcNameWithLineNumber(funcName, line) } return funcName } // isAnonymousFunction checks if the function name indicates an anonymous function. +// It does this by checking if the function name contains a dot (.) and starts with "func" after the last dot. +// If there isn't a dot in the function name, it checks if the function name starts with "func". func isAnonymousFunction(funcName string) bool { - parts := strings.Split(funcName, ".") - return strings.HasPrefix(parts[len(parts)-1], "func") + idx := strings.LastIndex(funcName, ".") + if idx > 0 { + return strings.HasPrefix(funcName[idx+1:], "func") + } + return strings.HasPrefix(funcName, "func") +} + +func funcNameWithLineNumber(funcName string, line int) string { + const lineNumberChars = 12 // " (line XXXX)" has 12 characters + var sb strings.Builder + sb.Grow(len(funcName) + lineNumberChars) + sb.WriteString(funcName) + sb.WriteString(" (line ") + sb.WriteString(strconv.Itoa(line)) + sb.WriteString(")") + return sb.String() +} + +func discardPackagePath(s string) string { + if idx := strings.LastIndex(s, "/"); idx >= 0 { + return s[idx+1:] + } + return s } diff --git a/with_test.go b/with_test.go index 9d6ac91..e2a62d0 100644 --- a/with_test.go +++ b/with_test.go @@ -1,35 +1,129 @@ -package errors_test +package errors import ( "testing" - - "github.com/arquivei/errors" ) func TestWithNoKeyValues(t *testing.T) { - rootErr := errors.New("some error") + rootErr := New("some error") - err := errors.With(rootErr) + err := With(rootErr) if err == nil { t.Fatal("expected non-nil error, got nil") } - var e errors.Error - if !errors.As(err, &e) { - t.Fatalf("expected error to be of type errors.Error, got %T", err) + var e Error + if !As(err, &e) { + t.Fatalf("expected error to be of type Error, got %T", err) } } func TestWithNoError(t *testing.T) { - err := errors.With(nil) + err := With(nil) if err != nil { t.Error("expected nil, got", err) } } func TestWith(t *testing.T) { - // receiving a single keyvalue will return a new error - err := errors.With(errors.New("some error"), errors.KV("key", "value")) - if _, ok := err.(errors.Error); !ok { - t.Error("expected errors.Error, got", err) + // With should return an Error type + err := With(New("some error"), KV("key", "value")) + if _, ok := err.(Error); !ok { + t.Error("expected Error, got", err) + } + + // Check if the error message is formatted correctly + err = With(New("some error"), KV("key", "value")) + if err.Error() != "errors.TestWith: some error {key: value}" { + t.Error("expected 'errors.TestWith: some error {key: value}', got", err.Error()) + } + + err = func() error { + return With(New("some error")) + }() + if err.Error() != "errors.TestWith.func1 (line 41): some error" { + t.Error("expected 'errors.TestWith.func1 (line 41): some error', got", err.Error()) + } + + // Force an anonymous function name + err = With(New("some error"), Op("customOp")) + if err.Error() != "customOp: some error" { + t.Error("expected 'customOp: some error', got", err.Error()) + } + + t.Run("UncomparableKey", func(t *testing.T) { + defer func() { + r := recover() + if r == nil { + t.Error("expected panic, got nil") + t.FailNow() + } + if err, ok := r.(error); ok { + if err.Error() != "key is not comparable" { + t.Errorf("expected 'key is not comparable', got %s", err.Error()) + } + } else { + t.Errorf("expected panic with error, got %v", r) + } + }() + _ = With(New("some error"), KV(func() {}, "value")) + }) +} + +func Test_isAnonymousFunction(t *testing.T) { + tests := []struct { + name string + want bool + }{ + {"", false}, + {"func1", true}, + {"package.func1", true}, + {"package.func2", true}, + {"package.SayHello", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := isAnonymousFunction(tt.name); got != tt.want { + t.Errorf("isAnonymousFunction(%q) = %v, want %v", tt.name, got, tt.want) + } + }) + } +} + +func Test_discardPackagePath(t *testing.T) { + tests := []struct { + name string + want string + }{ + {"", ""}, + {"func1", "func1"}, + {"package.func1", "package.func1"}, + {"path/package.SayHello", "package.SayHello"}, + {"path/SayHello", "SayHello"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := discardPackagePath(tt.name); got != tt.want { + t.Errorf("discardPackagePath(%q) = %q, want %q", tt.name, got, tt.want) + } + }) + } +} + +func Test_getWithCaller(t *testing.T) { + // Caso normal + result := getWithCaller() + if result == "" { + t.Error("Expected function name, got unknown") + } + + // Caso !ok + done := make(chan string) + go func() { + done <- getWithCaller() + }() + if result := <-done; result != "" { + t.Errorf("Expected , got %s", result) } }