Created
February 19, 2021 21:46
-
-
Save felipesere/e24ba5620255803866993c829aa82f34 to your computer and use it in GitHub Desktop.
Cute little diagnostics errors in go
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
package diagnostics | |
import ( | |
"errors" | |
"fmt" | |
"sort" | |
"strings" | |
) | |
type Diagnostic struct { | |
message string | |
data map[string]interface{} | |
innerErr *error | |
inner *Diagnostic | |
} | |
func (d Diagnostic) Error() string { | |
return "" | |
} | |
func (d Diagnostic) Is(err error) bool { | |
if d.innerErr != nil { | |
if errors.Is(*d.innerErr, err) { | |
return true | |
} | |
} | |
if d.inner != nil { | |
return d.inner.Is(err) | |
} | |
return false | |
} | |
func (d Diagnostic) As(val interface{}) bool { | |
if d.innerErr != nil { | |
if errors.As(*d.innerErr, val) { | |
return true | |
} | |
} | |
if d.inner != nil { | |
return d.inner.As(val) | |
} | |
return false | |
} | |
func (d Diagnostic) UserFacing(depth ...int) string { | |
start := 1 | |
if len(depth) > 0 { | |
start = depth[0] | |
} | |
var dataRep string | |
if d.data != nil { | |
dataRep = fmt.Sprintf(": %s", printable(d.data)) | |
} | |
if d.innerErr != nil { | |
return fmt.Sprintf("%s%s", *d.innerErr, dataRep) | |
} | |
if d.inner != nil { | |
indent := "" | |
for i := 0; i < start; i++ { | |
indent = fmt.Sprintf("\t%s", indent) | |
} | |
return fmt.Sprintf("%s%s\n%s└ %s", d.message, dataRep, indent, d.inner.UserFacing(start+1)) | |
} | |
return fmt.Sprintf("%s%s", d.message, dataRep) | |
} | |
func printable(data map[string]interface{}) string { | |
var keys []string | |
for k := range data { | |
keys = append(keys, k) | |
} | |
sort.Strings(keys) | |
var s []string | |
for _, key := range keys { | |
value := data[key] | |
if val, ok := value.(string); ok { | |
s = append(s, fmt.Sprintf("%s = %q", key, val)) | |
} else { | |
s = append(s, fmt.Sprintf("%s = %v", key, value)) | |
} | |
} | |
return strings.Join(s, ", ") | |
} | |
func (d Diagnostic) Wrap(message string) Diagnostic { | |
return Diagnostic{ | |
message: message, | |
data: nil, | |
innerErr: nil, | |
inner: &d, | |
} | |
} | |
func (d Diagnostic) WithData(key string, value interface{}) Diagnostic { | |
newData := d.data | |
if newData == nil { | |
newData = map[string]interface{}{} | |
} | |
newData[key] = value | |
return Diagnostic{ | |
message: d.message, | |
data: newData, | |
innerErr: d.innerErr, | |
inner: d.inner, | |
} | |
} | |
func (d Diagnostic) WithAllData(m map[string]interface{}) Diagnostic { | |
newData := d.data | |
if newData == nil { | |
newData = m | |
} else { | |
for k, v := range m { | |
newData[k] = v | |
} | |
} | |
return Diagnostic{ | |
message: d.message, | |
data: newData, | |
innerErr: d.innerErr, | |
inner: d.inner, | |
} | |
} | |
func FromString(message string) Diagnostic { | |
return Diagnostic{ | |
message: message, | |
data: nil, | |
inner: nil, | |
} | |
} | |
func FromErr(err error) Diagnostic { | |
return Diagnostic{ | |
message: "", | |
data: nil, | |
innerErr: &err, | |
} | |
} |
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
package diagnostics | |
import ( | |
"errors" | |
"fmt" | |
"github.com/stretchr/testify/assert" | |
"github.com/stretchr/testify/require" | |
"testing" | |
) | |
func TestCanBeCreated(t *testing.T) { | |
err := FromString("this failed") | |
output := err.UserFacing() | |
require.Equal(t, `this failed`, output) | |
} | |
func TestCanWrapAnInnerError(t *testing.T) { | |
err := FromErr(errors.New("some inner error")) | |
output := err.UserFacing() | |
require.Equal(t, `some inner error`, output) | |
} | |
func TestCanWrap(t *testing.T) { | |
err := FromErr(errors.New("some inner error")).Wrap("trying to do this") | |
output := err.UserFacing() | |
require.Equal(t, `trying to do this | |
└ some inner error`, output) | |
moreErr := err.Wrap("and some more context") | |
output = moreErr.UserFacing() | |
fmt.Println(output) | |
require.Equal(t, `and some more context | |
└ trying to do this | |
└ some inner error`, output) | |
} | |
func TestCanHoldSomeData(t *testing.T) { | |
err := FromErr(errors.New("some inner error")).WithData("foo", "bar").WithData("bar", 12) | |
output := err.UserFacing() | |
require.Equal(t, `some inner error: bar = 12, foo = "bar"`, output) | |
} | |
func TestCanHoldBunchOfData(t *testing.T) { | |
err := FromErr(errors.New("some inner error")).WithAllData(map[string]interface{}{ | |
"foo": "bar", | |
"bar": 12, | |
}) | |
output := err.UserFacing() | |
require.Equal(t, `some inner error: bar = 12, foo = "bar"`, output) | |
} | |
func TestDifferentPartsHoldData(t *testing.T) { | |
err := FromString("some inner error").WithData("foo", 12).Wrap("outer error").WithData("bar", true) | |
output := err.UserFacing() | |
fmt.Println(output) | |
require.Equal(t, `outer error: bar = true | |
└ some inner error: foo = 12`, output) | |
} | |
func TestIsStillAnError(t *testing.T) { | |
origin := errors.New("the origin") | |
wrapped := FromErr(origin).Wrap("layer").Wrap("deeper").Wrap("Very top") | |
assert.True(t, errors.Is(wrapped, origin)) | |
assert.False(t, errors.Is(wrapped, errors.New("random"))) | |
} | |
type SampleErr struct { | |
age int | |
} | |
func (s SampleErr) Error() string { | |
return fmt.Sprintf("The age is %d", s.age) | |
} | |
func TestExtracsDataAsError(t *testing.T) { | |
origin := SampleErr{age: 42} | |
wrapped := FromErr(origin).Wrap("layer").Wrap("deeper").Wrap("Very top") | |
var extracted SampleErr | |
assert.True(t, errors.As(wrapped, &extracted)) | |
assert.Equal(t, 42, extracted.age) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment