File: inspect.go

package info (click to toggle)
golang-github-olekukonko-errors 1.1.0-2
  • links: PTS, VCS
  • area: main
  • in suites: experimental, forky, sid
  • size: 500 kB
  • sloc: makefile: 2
file content (225 lines) | stat: -rw-r--r-- 6,587 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
// File: inspect.go
// Updated to support both error and *Error with delegation for cleaner *Error handling

package errors

import (
	stderrs "errors"
	"fmt"
	"strings"
	"time"
)

// Inspect provides detailed examination of an error, handling both single errors and MultiError
func Inspect(err error) {
	if err == nil {
		fmt.Println("No error occurred")
		return
	}

	fmt.Printf("\n=== Error Inspection ===\n")
	fmt.Printf("Top-level error: %v\n", err)
	fmt.Printf("Top-level error type: %T\n", err)

	// Handle *Error directly
	if e, ok := err.(*Error); ok {
		InspectError(e)
		return
	}

	// Handle MultiError
	if multi, ok := err.(*MultiError); ok {
		allErrors := multi.Errors()
		fmt.Printf("\nContains %d errors:\n", len(allErrors))
		for i, e := range allErrors {
			fmt.Printf("\n--- Error %d ---\n", i+1)
			inspectSingleError(e)
		}
	} else {
		// Inspect single error if not MultiError or *Error
		fmt.Println("\n--- Details ---")
		inspectSingleError(err)
	}

	// Additional diagnostics
	fmt.Println("\n--- Diagnostics ---")
	if IsRetryable(err) {
		fmt.Println("- Error chain contains retryable errors")
	}
	if IsTimeout(err) {
		fmt.Println("- Error chain contains timeout errors")
	}
	if code := getErrorCode(err); code != 0 {
		fmt.Printf("- Highest priority error code: %d\n", code)
	}
	fmt.Printf("========================\n\n")
}

// InspectError provides detailed inspection of a specific *Error instance
func InspectError(err *Error) {
	if err == nil {
		fmt.Println("No error occurred")
		return
	}

	fmt.Printf("\n=== Error Inspection (*Error) ===\n")
	fmt.Printf("Top-level error: %v\n", err)
	fmt.Printf("Top-level error type: %T\n", err)

	fmt.Println("\n--- Details ---")
	inspectSingleError(err) // Delegate to handle unwrapping and details

	// Additional diagnostics specific to *Error
	fmt.Println("\n--- Diagnostics ---")
	if IsRetryable(err) {
		fmt.Println("- Error is retryable")
	}
	if IsTimeout(err) {
		fmt.Println("- Error chain contains timeout errors")
	}
	if code := err.Code(); code != 0 {
		fmt.Printf("- Error code: %d\n", code)
	}
	fmt.Printf("========================\n\n")
}

// inspectSingleError handles inspection of a single error (may be part of a chain)
func inspectSingleError(err error) {
	if err == nil {
		fmt.Println("  (nil error)")
		return
	}

	fmt.Printf("  Error: %v\n", err)
	fmt.Printf("  Type: %T\n", err)

	// Handle wrapped errors, including *Error type
	var currentErr error = err
	depth := 0
	for currentErr != nil {
		prefix := strings.Repeat("  ", depth+1)
		if depth > 0 {
			fmt.Printf("%sWrapped Cause (%T): %v\n", prefix, currentErr, currentErr)
		}

		// Check if it's our specific *Error type
		if e, ok := currentErr.(*Error); ok {
			if name := e.Name(); name != "" {
				fmt.Printf("%sName: %s\n", prefix, name)
			}
			if cat := e.Category(); cat != "" {
				fmt.Printf("%sCategory: %s\n", prefix, cat)
			}
			if code := e.Code(); code != 0 {
				fmt.Printf("%sCode: %d\n", prefix, code)
			}
			if ctx := e.Context(); len(ctx) > 0 {
				fmt.Printf("%sContext:\n", prefix)
				for k, v := range ctx {
					fmt.Printf("%s  %s: %v\n", prefix, k, v)
				}
			}
			if stack := e.Stack(); len(stack) > 0 {
				fmt.Printf("%sStack (Top 3):\n", prefix)
				limit := 3
				if len(stack) < limit {
					limit = len(stack)
				}
				for i := 0; i < limit; i++ {
					fmt.Printf("%s  %s\n", prefix, stack[i])
				}
				if len(stack) > limit {
					fmt.Printf("%s  ... (%d more frames)\n", prefix, len(stack)-limit)
				}
			}
		}

		// Unwrap using standard errors.Unwrap and handle *Error Unwrap
		var nextErr error
		// Prioritize *Error's Unwrap if available AND it returns non-nil
		if e, ok := currentErr.(*Error); ok {
			unwrapped := e.Unwrap()
			if unwrapped != nil {
				nextErr = unwrapped
			} else {
				// If *Error.Unwrap returns nil, fall back to standard unwrap
				// This handles cases where *Error might wrap a non-standard error
				// or where its internal cause is deliberately nil.
				nextErr = stderrs.Unwrap(currentErr)
			}
		} else {
			nextErr = stderrs.Unwrap(currentErr) // Fall back to standard unwrap for non-*Error types
		}

		// Prevent infinite loops if Unwrap returns the same error, or stop if no more unwrapping
		if nextErr == currentErr || nextErr == nil {
			break
		}
		currentErr = nextErr
		depth++
		if depth > 10 { // Safety break for very deep or potentially cyclic chains
			fmt.Printf("%s... (chain too deep or potential cycle)\n", strings.Repeat("  ", depth+1))
			break
		}
	}
}

// getErrorCode traverses the error chain to find the highest priority code.
// It uses errors.As to find the first *Error in the chain.
func getErrorCode(err error) int {
	var code int = 0 // Default code
	var target *Error
	if As(err, &target) { // Use the package's As helper
		if target != nil { // Add nil check for safety
			code = target.Code()
		}
	}
	// If the top-level error is *Error and has a code, it might take precedence.
	// This depends on desired logic. Let's keep it simple for now: first code found by As.
	if code == 0 { // Only check top-level if As didn't find one with a code
		if e, ok := err.(*Error); ok {
			code = e.Code()
		}
	}
	return code
}

// handleError demonstrates using Inspect with additional handling logic
func handleError(err error) {
	fmt.Println("\n=== Processing Failure ===")
	Inspect(err) // Use the primary Inspect function

	// Additional handling based on inspection
	code := getErrorCode(err) // Use the helper

	switch {
	case IsTimeout(err):
		fmt.Println("\nAction: Check connectivity or increase timeout")
	case code == 402: // Check code obtained via helper
		fmt.Println("\nAction: Payment processing failed - notify billing")
	default:
		fmt.Println("\nAction: Generic failure handling")
	}
}

// processOrder demonstrates Chain usage with Inspect
func processOrder() error {
	validateInput := func() error { return nil }
	processPayment := func() error { return stderrs.New("credit card declined") }
	sendNotification := func() error { fmt.Println("Notification sent."); return nil }
	logOrder := func() error { fmt.Println("Order logged."); return nil }

	chain := NewChain(ChainWithTimeout(2*time.Second)).
		Step(validateInput).Tag("validation").
		Step(processPayment).Tag("billing").Code(402).Retry(3, 100*time.Millisecond, WithRetryIf(IsRetryable)).
		Step(sendNotification).Optional().
		Step(logOrder)

	err := chain.Run()
	if err != nil {
		handleError(err) // Call the unified error handler
		return err       // Propagate the error if needed
	}
	fmt.Println("Order processed successfully!")
	return nil
}