Skip to content

Commit 9d2d5fd

Browse files
committed
Add "Printable" properties
Add properties that will be included into error message.
1 parent 9fd1d14 commit 9d2d5fd

File tree

3 files changed

+110
-18
lines changed

3 files changed

+110
-18
lines changed

error.go

Lines changed: 52 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@ type Error struct {
1616
stackTrace *stackTrace
1717
// properties are used both for public properties inherited through "transparent" wrapping
1818
// and for some optional per-instance information like "underlying errors"
19-
properties *propertyMap
20-
transparent bool
21-
hasUnderlying bool
19+
properties *propertyMap
20+
21+
transparent bool
22+
hasUnderlying bool
23+
printablePropertyCount uint8
2224
}
2325

2426
var _ fmt.Formatter = (*Error)(nil)
@@ -31,6 +33,9 @@ var _ fmt.Formatter = (*Error)(nil)
3133
func (e *Error) WithProperty(key Property, value interface{}) *Error {
3234
errorCopy := *e
3335
errorCopy.properties = errorCopy.properties.with(key, value)
36+
if key.printable && errorCopy.printablePropertyCount < 255 {
37+
errorCopy.printablePropertyCount++
38+
}
3439
return &errorCopy
3540
}
3641

@@ -205,6 +210,25 @@ func (e *Error) underlyingInfo() string {
205210
return fmt.Sprintf("(hidden: %s)", joinStringsIfNonEmpty(", ", infos...))
206211
}
207212

213+
func (e *Error) messageFromProperties() string {
214+
if e.printablePropertyCount == 0 {
215+
return ""
216+
}
217+
uniq := make(map[Property]struct{}, e.printablePropertyCount)
218+
strs := make([]string, 0, e.printablePropertyCount)
219+
for m := e.properties; m != nil; m = m.next {
220+
if !m.p.printable {
221+
continue
222+
}
223+
if _, ok := uniq[m.p]; ok {
224+
continue
225+
}
226+
uniq[m.p] = struct{}{}
227+
strs = append(strs, fmt.Sprintf("%s: %v", m.p.label, m.value))
228+
}
229+
return "{" + strings.Join(strs, ", ") + "}"
230+
}
231+
208232
func (e *Error) underlying() []error {
209233
if !e.hasUnderlying {
210234
return nil
@@ -216,21 +240,35 @@ func (e *Error) underlying() []error {
216240
}
217241

218242
func (e *Error) messageText() string {
219-
if e.Cause() == nil {
220-
return e.message
243+
message := joinStringsIfNonEmpty(" ", e.message, e.messageFromProperties())
244+
if cause := e.Cause(); cause != nil {
245+
return joinStringsIfNonEmpty(", cause: ", message, cause.Error())
221246
}
222-
223-
underlyingFullMessage := e.Cause().Error()
224-
return joinStringsIfNonEmpty(", cause: ", e.message, underlyingFullMessage)
247+
return message
225248
}
226249

227250
func joinStringsIfNonEmpty(delimiter string, parts ...string) string {
228-
filteredParts := make([]string, 0, len(parts))
229-
for _, part := range parts {
230-
if len(part) > 0 {
231-
filteredParts = append(filteredParts, part)
251+
switch len(parts) {
252+
case 0:
253+
return ""
254+
case 1:
255+
return parts[0]
256+
case 2:
257+
if len(parts[0]) == 0 {
258+
return parts[1]
259+
} else if len(parts[1]) == 0 {
260+
return parts[0]
261+
} else {
262+
return parts[0] + delimiter + parts[1]
263+
}
264+
default:
265+
filteredParts := make([]string, 0, len(parts))
266+
for _, part := range parts {
267+
if len(part) > 0 {
268+
filteredParts = append(filteredParts, part)
269+
}
232270
}
233-
}
234271

235-
return strings.Join(filteredParts, delimiter)
272+
return strings.Join(filteredParts, delimiter)
273+
}
236274
}

property.go

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,21 @@ type Property struct {
1212
}
1313

1414
type property struct {
15-
label string
15+
label string
16+
printable bool
1617
}
1718

1819
// RegisterProperty registers a new property key.
1920
// It is used both to add a dynamic property to an error instance, and to extract property value back from error.
2021
func RegisterProperty(label string) Property {
21-
return newProperty(label)
22+
return newProperty(label, false)
23+
}
24+
25+
// RegisterPrintableProperty registers a new property key for informational value.
26+
// It is used both to add a dynamic property to an error instance, and to extract property value back from error.
27+
// Informational property will be included in Error() message.
28+
func RegisterPrintableProperty(label string) Property {
29+
return newProperty(label, true)
2230
}
2331

2432
// PropertyContext is a context property, value is expected to be of context.Context type.
@@ -73,10 +81,11 @@ var (
7381
propertyUnderlying = RegisterProperty("underlying")
7482
)
7583

76-
func newProperty(label string) Property {
84+
func newProperty(label string, printable bool) Property {
7785
p := Property{
7886
&property{
79-
label: label,
87+
label: label,
88+
printable: printable,
8089
},
8190
}
8291
return p

property_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"testing"
66

7+
"github.com/stretchr/testify/assert"
78
"github.com/stretchr/testify/require"
89
)
910

@@ -33,6 +34,8 @@ func TestNoProperty(t *testing.T) {
3334

3435
var testProperty0 = RegisterProperty("test0")
3536
var testProperty1 = RegisterProperty("test1")
37+
var testInfoProperty2 = RegisterPrintableProperty("prop2")
38+
var testInfoProperty3 = RegisterPrintableProperty("prop3")
3639

3740
func TestProperty(t *testing.T) {
3841
t.Run("Different", func(t *testing.T) {
@@ -109,6 +112,48 @@ func TestProperty(t *testing.T) {
109112
})
110113
}
111114

115+
func TestPrintableProperty(t *testing.T) {
116+
err := testTypeSilent.New("test").WithProperty(testInfoProperty2, "hello world")
117+
t.Run("Simple", func(t *testing.T) {
118+
assert.Equal(t, "foo.bar.silent: test {prop2: hello world}", err.Error())
119+
})
120+
121+
t.Run("Overwrite", func(t *testing.T) {
122+
err := err.WithProperty(testInfoProperty2, "cruel world")
123+
assert.Equal(t, "foo.bar.silent: test {prop2: cruel world}", err.Error())
124+
})
125+
126+
t.Run("AddMore", func(t *testing.T) {
127+
err := err.WithProperty(testInfoProperty3, struct{ a int }{1})
128+
assert.Equal(t, "foo.bar.silent: test {prop3: {1}, prop2: hello world}", err.Error())
129+
})
130+
131+
t.Run("NonPrintableIsInvisible", func(t *testing.T) {
132+
err := err.WithProperty(testProperty0, "nah")
133+
assert.Equal(t, "foo.bar.silent: test {prop2: hello world}", err.Error())
134+
})
135+
136+
t.Run("WithUnderlying", func(t *testing.T) {
137+
err := err.WithUnderlyingErrors(testTypeSilent.New("underlying"))
138+
assert.Equal(t, "foo.bar.silent: test {prop2: hello world} (hidden: foo.bar.silent: underlying)", err.Error())
139+
})
140+
141+
err2 := Decorate(err, "oops")
142+
t.Run("Decorate", func(t *testing.T) {
143+
assert.Equal(t, "oops, cause: foo.bar.silent: test {prop2: hello world}", err2.Error())
144+
})
145+
146+
t.Run("DecorateAndAddMore", func(t *testing.T) {
147+
err := err2.WithProperty(testInfoProperty3, struct{ a int }{1})
148+
assert.Equal(t, "oops {prop3: {1}}, cause: foo.bar.silent: test {prop2: hello world}", err.Error())
149+
})
150+
151+
t.Run("DecorateAndAddSame", func(t *testing.T) {
152+
err := err2.WithProperty(testInfoProperty2, "cruel world")
153+
assert.Equal(t, "oops {prop2: cruel world}, cause: foo.bar.silent: test {prop2: hello world}", err.Error())
154+
})
155+
}
156+
112157
func BenchmarkAllocProperty(b *testing.B) {
113158
const N = 9
114159
var properties = []Property{}

0 commit comments

Comments
 (0)