format.go

v0.2.0
Doc Versions Source
1
package culpa
2
3
import (
4
	"fmt"
5
	"log/slog"
6
	"reflect"
7
	"strings"
8
)
9
10
// Format implements [fmt.Formatter] for culpaError.
11
//
12
//   - %v / %s — message chain only (same as Error())
13
//   - %+v     — full dump: message chain, details, stacktraces
14
func (e *culpaError) Format(s fmt.State, verb rune) { formatError(s, verb, e) }
15
16
// Format implements [fmt.Formatter] for detailsWrap.
17
func (d *detailsWrap) Format(s fmt.State, verb rune) { formatError(s, verb, d) }
18
19
// Format implements [fmt.Formatter] for joinedError.
20
func (e *joinedError) Format(s fmt.State, verb rune) { formatError(s, verb, e) }
21
22
func formatError(s fmt.State, verb rune, err error) {
23
	switch verb {
24
	case 'v':
25
		if s.Flag('+') {
26
			fmt.Fprint(s, err.Error())
27
			printDetails(s, err)
28
			return
29
		}
30
		fallthrough
31
	case 's':
32
		fmt.Fprint(s, err.Error())
33
	case 'q':
34
		fmt.Fprintf(s, "%q", err.Error())
35
	}
36
}
37
38
func printDetails(w fmt.State, err error) {
39
	walkTree(err, func(e error) bool {
40
		if dw, ok := e.(*detailsWrap); ok {
41
			for _, d := range dw.details {
42
				if st, ok := d.(StacktraceDetail); ok {
43
					fmt.Fprintf(w, "\n--- stacktrace ---\n%+v", st.Trace)
44
					continue
45
				}
46
				fmt.Fprintf(w, "\n  %s: %+v", detailTypeName(d), d)
47
			}
48
		}
49
		return true
50
	})
51
}
52
53
// LogValue implements [slog.LogValuer] so that culpa errors produce
54
// structured output when passed to slog.
55
func (e *culpaError) LogValue() slog.Value { return logValue(e) }
56
57
// LogValue implements [slog.LogValuer] for detailsWrap.
58
func (d *detailsWrap) LogValue() slog.Value { return logValue(d) }
59
60
// LogValue implements [slog.LogValuer] for joinedError.
61
func (e *joinedError) LogValue() slog.Value { return logValue(e) }
62
63
func logValue(err error) slog.Value {
64
	attrs := []slog.Attr{slog.String("msg", err.Error())}
65
66
	seen := make(map[reflect.Type]bool)
67
	walkTree(err, func(e error) bool {
68
		if dw, ok := e.(*detailsWrap); ok {
69
			for _, d := range dw.details {
70
				dt := reflect.TypeOf(d)
71
				if dt == reflect.TypeOf(StacktraceDetail{}) {
72
					continue
73
				}
74
				if seen[dt] {
75
					continue
76
				}
77
				seen[dt] = true
78
				key := detailKey(dt)
79
				attrs = append(attrs, slog.Any(key, d))
80
			}
81
		}
82
		return true
83
	})
84
85
	// Output stacktrace.Trace directly so handlers can format as needed
86
	traces := FindDetails[StacktraceDetail](err)
87
	if len(traces) > 0 {
88
		if len(traces) == 1 {
89
			attrs = append(attrs, slog.Any("stacktrace", traces[0].Trace))
90
		} else {
91
			// Multiple traces - collect them
92
			traceVals := make([]any, len(traces))
93
			for i, st := range traces {
94
				traceVals[i] = st.Trace
95
			}
96
			attrs = append(attrs, slog.Any("stacktrace", traceVals))
97
		}
98
	}
99
100
	return slog.GroupValue(attrs...)
101
}
102
103
func detailTypeName(d any) string {
104
	t := reflect.TypeOf(d)
105
	if t.Kind() == reflect.Pointer {
106
		t = t.Elem()
107
	}
108
	return t.Name()
109
}
110
111
func detailKey(t reflect.Type) string {
112
	if t.Kind() == reflect.Pointer {
113
		t = t.Elem()
114
	}
115
	name := t.Name()
116
	name = strings.TrimSuffix(name, "Detail")
117
	if name == "" {
118
		return t.Name()
119
	}
120
	return strings.ToLower(name[:1]) + name[1:]
121
}
122

Source Files