Skip to content

Instantly share code, notes, and snippets.

@felipesere
Created February 19, 2021 21:46
Show Gist options
  • Save felipesere/e24ba5620255803866993c829aa82f34 to your computer and use it in GitHub Desktop.
Save felipesere/e24ba5620255803866993c829aa82f34 to your computer and use it in GitHub Desktop.
Cute little diagnostics errors in go
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,
}
}
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