Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 4 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,16 @@ jobs:

build:
runs-on: ubuntu-latest
strategy:
matrix:
go-version: [1.21, 1.22, 1.23]
steps:
- uses: actions/checkout@v3

- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.19
go-version: ${{ matrix.go-version }}
- name: Test
run: go test -v -coverprofile=coverage.txt -covermode=count ./...
- name: Upload coverage reports to Codecov
Expand Down
34 changes: 30 additions & 4 deletions builder.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package errs

import "fmt"
import (
"errors"
"fmt"
"strings"
)

// B returns a new error builder.
// Optionally, you can pass an error instance to the builder, ONLY the first error will be used.
Expand All @@ -22,6 +26,15 @@ func B(initial ...error) *Builder {
return &Builder{err}
}

// WrapB wraps an underlying error and returns a builder for the new error.
func WrapB(err error) *Builder {
var cause *Error
errors.As(err, &cause)
b := B(nil)
b.err.wrap(cause)
return b
}

// Builder is used to build an instance of `Error` object.
type Builder struct {
err *Error
Expand All @@ -35,7 +48,7 @@ func (b *Builder) Code(code Code) *Builder {

// Msg sets the message of the error.
func (b *Builder) Msg(msg ...string) *Builder {
b.err.Msg = append(b.err.Msg, rmNilStr(msg)...)
b.err.Msg = append(b.err.Msg, cleanStrings(msg)...)
return b
}

Expand All @@ -45,11 +58,11 @@ func (b *Builder) Op(op string) *Builder {
return b
}

func rmNilStr(s []string) []string {
func cleanStrings(s []string) []string {
var r []string
for _, v := range s {
if v != "" {
r = append(r, v)
r = append(r, strings.TrimSpace(v))
}
}
return r
Expand All @@ -67,6 +80,19 @@ func (b *Builder) Details(details ...any) *Builder {
return b
}

// Show sets the show flag of the error.
// If this error is wrapped by another error, it's shown to users.
func (b *Builder) Show() *Builder {
old := b.err.show
if old {
return b
}

b.err.shownDepth++
b.err.show = true
return b
}

// Err returns new instance of `Error`.
func (b *Builder) Err() error {
return b.err
Expand Down
41 changes: 36 additions & 5 deletions builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,9 @@ import (

func ExampleBuilder() {
b := B().Code(Unknown).Msg("unknown error").Details("details")
err := b.Err()
str, _ := json.Marshal(err)
fmt.Println(string(str))
fmt.Println(b.Err())

// Output: {"code":"unknown","message":["unknown error"],"op":""}
// Output: unknown: unknown error
}

func ExampleBuilder_Msgf() {
Expand All @@ -24,7 +22,40 @@ func ExampleBuilder_Msgf() {
str, _ := json.Marshal(err)
fmt.Println(string(str))

// Output: {"code":"not_found","message":["file not found: details, 123"],"op":"File.Open"}
// Output: {"op":"File.Open","message":["file not found: details, 123"],"code":"not_found"}
}

func TestWrapB(t *testing.T) {
tests := []struct {
// given
inner error
name string
expectDepth int
expectShownDepth int
expectErr string
}{
{
name: "wrapping nil error",
inner: nil,
expectErr: "unknown",
},
{
name: "wrapping existing wrapped error",
inner: Wrap(B().Msg("inner0").Show().Err(), B().Msg("inner1").Err()),
expectDepth: 2,
expectShownDepth: 1,
expectErr: "unknown\nunknown: inner0",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := WrapB(tt.inner)
assert.Equal(t, tt.expectDepth, got.err.depth)
assert.Equal(t, tt.expectShownDepth, got.err.shownDepth)
assert.Equal(t, tt.expectErr, got.err.Error())
})
}
}

func TestBuilder(t *testing.T) {
Expand Down
118 changes: 87 additions & 31 deletions error.go
Original file line number Diff line number Diff line change
@@ -1,52 +1,95 @@
package errs

import (
"errors"
"fmt"
"io"
"strings"
)

type Error struct {
// Code is the error code of the error. When marshaled to JSON, it will be a string.
Code Code `json:"code"`
// Separator is the default separator between elements of a single error
var Separator = ": "

// Msg is the user-friendly message returned to the client.
Msg []string `json:"message"`
type Error struct {
// underlying error
cause *Error

// Op operation where error occured
Op string `json:"op"`

// Msg is the user-friendly message returned to the client.
Msg []string `json:"message"`

// Details is the internal error message returned to the developer.
Details []any `json:"-"`

// underlying error
cause *Error
// Code is the error code of the error. When marshaled to JSON, it will be a string.
Code Code `json:"code"`

// show is a flag that indicates whether the error would be visible when wrapped by another error
show bool

// depth of the error tree
depth int
depth int
shownDepth int
}

// Error returns the error in the format "code: message".
// Error returns the error in the format "code: message\ninner_code: inner_message" for this error and SHOWN underlying errors.
func (e *Error) Error() string {
return e.Code.String() + ": " + e.Op + ": " + strings.Join(e.Msg, ": ")
if e == nil {
return ""
}

buf := strings.Builder{}
e.writeTo(&buf)

if e.cause != nil {
// print the underlying error only if shown
for _, inner := range shown(e.cause) {
buf.WriteString("\n")
inner.writeTo(&buf)
}
}

return buf.String()
}

// String returns the error in the format "code: message".
func (e *Error) String() string {
var buf strings.Builder
e.writeTo(&buf)
return buf.String()
}

func (e *Error) writeTo(w io.StringWriter) {
w.WriteString(e.Code.String())
if len(e.Op) > 0 {
w.WriteString(Separator + e.Op)
}

msgs := strings.Join(cleanStrings(e.Msg), Separator)
if len(msgs) > 0 {
w.WriteString(Separator + msgs)
}
}

// Stack returns a description of the error and all it's underlying errors.
func (e *Error) Stack() []string {
stack := make([]string, e.depth+1)
for i, err := 0, e; err != nil; err, i = err.cause, i+1 {
func (e *Error) Stack() string {
var buf strings.Builder
for i, er := range all(e) {
tabOffset := strings.Repeat("\t", i)
var buf strings.Builder
write := func(s string) {
buf.WriteString(tabOffset)
buf.WriteString(s)
}
write(err.Error() + "\n")
for dx, d := range err.Details {
write(er.String() + "\n")
for dx, d := range er.Details {
write(fmt.Sprintf("\t%d: %v\n", dx, d))
}
stack[i] = buf.String()
buf.WriteString("\n")
}
return stack

return buf.String()
}

// Unwrap returns the underlying error.
Expand All @@ -57,8 +100,21 @@ func (e *Error) Unwrap() error {
return e.cause
}

func (e *Error) wrap(inner *Error) {
e.cause = inner
if inner == nil {
return
}
e.depth = inner.depth + 1
e.shownDepth = inner.shownDepth
if e.show {
e.shownDepth++
}
}

func (e *Error) Is(target error) bool {
if t, ok := target.(*Error); ok {
var t *Error
if errors.As(target, &t) {
return equalNodes(e, t)
}
return false
Expand Down Expand Up @@ -123,8 +179,7 @@ func Wrap(child, parent error) error {
default:
p := parent.(*Error)
c := child.(*Error)
p.cause = c
p.depth = c.depth + 1
p.wrap(c)
return p
}
}
Expand All @@ -135,7 +190,8 @@ func Convert(err error) error {
if err == nil {
return nil
}
if e, ok := err.(*Error); ok {
var e *Error
if errors.As(err, &e) {
return e
}
return &Error{
Expand All @@ -146,15 +202,15 @@ func Convert(err error) error {

// WrapCode wraps an underlying error with a new error, adding message to the error's previously existing message and setting the error code to code.
func WrapCode(err error, code Code, messages ...string) error {
er := Convert(err)
if er == nil {
return B().Code(code).Msg(rmNilStr(messages)...).Err()
err = Convert(err)
if err == nil {
return B().Code(code).Msg(cleanStrings(messages)...).Err()
}
e := er.(*Error)
return &Error{
Code: code,
cause: e,
Msg: rmNilStr(messages),
depth: e.depth + 1,
er := err.(*Error)
e := &Error{
Code: code,
Msg: cleanStrings(messages),
}
e.wrap(er)
return e
}
Loading