detail.go

v0.2.0
Doc Versions Source
1
package culpa
2
3
import "reflect"
4
5
// MergeStrategy defines how duplicate detail types are resolved
6
// when the same type appears in multiple layers of the error chain.
7
type MergeStrategy int
8
9
const (
10
	// MergeOverwrite — outermost (latest) value wins. Default.
11
	MergeOverwrite MergeStrategy = iota
12
	// MergeCombine — values from all layers are collected into a slice.
13
	MergeCombine
14
)
15
16
// MergeStrategyProvider is implemented by detail types that declare
17
// a non-default merge strategy.
18
type MergeStrategyProvider interface {
19
	MergeStrategy() MergeStrategy
20
}
21
22
// detailsWrap is an error wrapper that carries attached details.
23
// It has no message of its own — it is transparent in the message chain.
24
type detailsWrap struct {
25
	cause   error
26
	details []any
27
}
28
29
// Error delegates to the cause.
30
func (d *detailsWrap) Error() string {
31
	if d.cause == nil {
32
		return ""
33
	}
34
	return d.cause.Error()
35
}
36
37
// Unwrap returns the wrapped cause.
38
func (d *detailsWrap) Unwrap() error {
39
	return d.cause
40
}
41
42
// WithDetail attaches a single detail to the error.
43
// Returns nil if err is nil.
44
func WithDetail(err error, detail any) error {
45
	if err == nil {
46
		return nil
47
	}
48
	return &detailsWrap{cause: err, details: []any{detail}}
49
}
50
51
// WithDetails attaches multiple details to the error in a single layer.
52
// Returns nil if err is nil.
53
func WithDetails(err error, details ...any) error {
54
	if err == nil {
55
		return nil
56
	}
57
	if len(details) == 0 {
58
		return err
59
	}
60
	return &detailsWrap{cause: err, details: details}
61
}
62
63
// FindDetail finds the first detail of type T in the error tree
64
// (pre-order, depth-first) and writes it into dst. dst must be a non-nil
65
// pointer. Returns true if found.
66
func FindDetail(err error, dst any) bool {
67
	dstVal := reflect.ValueOf(dst)
68
	if dstVal.Kind() != reflect.Pointer || dstVal.IsNil() {
69
		return false
70
	}
71
	target := dstVal.Elem().Type()
72
73
	var found bool
74
	walkTree(err, func(e error) bool {
75
		if dw, ok := e.(*detailsWrap); ok {
76
			for _, d := range dw.details {
77
				if reflect.TypeOf(d) == target {
78
					dstVal.Elem().Set(reflect.ValueOf(d))
79
					found = true
80
					return false
81
				}
82
			}
83
		}
84
		return true
85
	})
86
	return found
87
}
88
89
// FindDetails finds all details of type T in the error tree.
90
func FindDetails[T any](err error) []T {
91
	var result []T
92
	walkTree(err, func(e error) bool {
93
		if dw, ok := e.(*detailsWrap); ok {
94
			for _, d := range dw.details {
95
				if v, ok := d.(T); ok {
96
					result = append(result, v)
97
				}
98
			}
99
		}
100
		return true
101
	})
102
	return result
103
}
104
105
// AllDetails returns all details from the error tree as a flat list.
106
func AllDetails(err error) []any {
107
	var result []any
108
	walkTree(err, func(e error) bool {
109
		if dw, ok := e.(*detailsWrap); ok {
110
			result = append(result, dw.details...)
111
		}
112
		return true
113
	})
114
	return result
115
}
116
117
// walkTree performs a pre-order, depth-first traversal of the error tree.
118
// It handles both single-unwrap (Unwrap() error) and multi-unwrap
119
// (Unwrap() []error) errors. Stops early if fn returns false.
120
func walkTree(err error, fn func(error) bool) {
121
	if err == nil {
122
		return
123
	}
124
	stack := []error{err}
125
	for len(stack) > 0 {
126
		e := stack[len(stack)-1]
127
		stack = stack[:len(stack)-1]
128
		if !fn(e) {
129
			return
130
		}
131
		switch u := e.(type) {
132
		case interface{ Unwrap() error }:
133
			if next := u.Unwrap(); next != nil {
134
				stack = append(stack, next)
135
			}
136
		case interface{ Unwrap() []error }:
137
			errs := u.Unwrap()
138
			// Push in reverse order so the first child is visited first.
139
			for i := len(errs) - 1; i >= 0; i-- {
140
				if errs[i] != nil {
141
					stack = append(stack, errs[i])
142
				}
143
			}
144
		}
145
	}
146
}
147

Source Files