File: multi_error.go

package info (click to toggle)
golang-github-olekukonko-errors 1.1.0-1
  • links: PTS, VCS
  • area: main
  • in suites: experimental, forky, sid
  • size: 448 kB
  • sloc: makefile: 2
file content (423 lines) | stat: -rw-r--r-- 11,140 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
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
package errors

import (
	"bytes"
	"encoding/json"
	"fmt"
	"math/rand"
	"strings"
	"sync"
	"sync/atomic"
)

// MultiError represents a thread-safe collection of errors with enhanced features.
// Supports limits, sampling, and custom formatting for error aggregation.
type MultiError struct {
	errors []error
	mu     sync.RWMutex

	// Configuration fields
	limit      int            // Maximum number of errors to store (0 = unlimited)
	formatter  ErrorFormatter // Custom formatting function for error string
	sampling   bool           // Whether sampling is enabled to limit error collection
	sampleRate uint32         // Sampling percentage (1-100) when sampling is enabled
	rand       *rand.Rand     // Random source for sampling (nil defaults to fastRand)
}

// ErrorFormatter defines a function for custom error message formatting.
// Takes a slice of errors and returns a single formatted string.
type ErrorFormatter func([]error) string

// MultiErrorOption configures MultiError behavior during creation.
type MultiErrorOption func(*MultiError)

// NewMultiError creates a new MultiError instance with optional configuration.
// Initial capacity is set to 4; applies options in the order provided.
func NewMultiError(opts ...MultiErrorOption) *MultiError {
	m := &MultiError{
		errors: make([]error, 0, 4),
		limit:  0, // Unlimited by default
	}

	for _, opt := range opts {
		opt(m)
	}
	return m
}

// Add appends an error to the collection with optional sampling, limit checks, and duplicate prevention.
// Ignores nil errors and duplicates based on string equality; thread-safe.
func (m *MultiError) Add(errs ...error) {
	if len(errs) == 0 {
		return
	}

	m.mu.Lock()
	defer m.mu.Unlock()

	for _, err := range errs {
		if err == nil {
			continue
		}

		// Check for duplicates by comparing error messages
		duplicate := false
		for _, e := range m.errors {
			if e.Error() == err.Error() {
				duplicate = true
				break
			}
		}
		if duplicate {
			continue
		}

		// Apply sampling if enabled and collection isn’t empty
		if m.sampling && len(m.errors) > 0 {
			var r uint32
			if m.rand != nil {
				r = uint32(m.rand.Int31n(100))
			} else {
				r = fastRand() % 100
			}
			if r > m.sampleRate { // Accept if random value is within sample rate
				continue
			}
		}

		// Respect limit if set
		if m.limit > 0 && len(m.errors) >= m.limit {
			continue
		}

		m.errors = append(m.errors, err)
	}
}

// Addf formats and adds a new error to the collection.
func (m *MultiError) Addf(format string, args ...interface{}) {
	m.Add(Newf(format, args...))
}

// Clear removes all errors from the collection.
// Thread-safe; resets the slice while preserving capacity.
func (m *MultiError) Clear() {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.errors = m.errors[:0]
}

// Count returns the number of errors in the collection.
// Thread-safe.
func (m *MultiError) Count() int {
	m.mu.RLock()
	defer m.mu.RUnlock()
	return len(m.errors)
}

// Error returns a formatted string representation of the errors.
// Returns empty string if no errors, single error message if one exists,
// or a formatted list using custom formatter or default if multiple; thread-safe.
func (m *MultiError) Error() string {
	m.mu.RLock()
	defer m.mu.RUnlock()

	switch len(m.errors) {
	case 0:
		return ""
	case 1:
		return m.errors[0].Error()
	default:
		if m.formatter != nil {
			return m.formatter(m.errors)
		}
		return defaultFormat(m.errors)
	}
}

// Errors returns a copy of the contained errors.
// Thread-safe; returns nil if no errors exist.
func (m *MultiError) Errors() []error {
	m.mu.RLock()
	defer m.mu.RUnlock()

	if len(m.errors) == 0 {
		return nil
	}
	errs := make([]error, len(m.errors))
	copy(errs, m.errors)
	return errs
}

// Filter returns a new MultiError containing only errors that match the predicate.
// Thread-safe; preserves original configuration including limit, formatter, and sampling.
func (m *MultiError) Filter(fn func(error) bool) *MultiError {
	m.mu.RLock()
	defer m.mu.RUnlock()

	var opts []MultiErrorOption
	opts = append(opts, WithLimit(m.limit))
	if m.formatter != nil {
		opts = append(opts, WithFormatter(m.formatter))
	}
	if m.sampling {
		opts = append(opts, WithSampling(m.sampleRate))
	}

	filtered := NewMultiError(opts...)
	for _, err := range m.errors {
		if fn(err) {
			filtered.Add(err)
		}
	}
	return filtered
}

// First returns the first error in the collection, if any.
// Thread-safe; returns nil if the collection is empty.
func (m *MultiError) First() error {
	m.mu.RLock()
	defer m.mu.RUnlock()
	if len(m.errors) > 0 {
		return m.errors[0]
	}
	return nil
}

// Has reports whether the collection contains any errors.
// Thread-safe.
func (m *MultiError) Has() bool {
	m.mu.RLock()
	defer m.mu.RUnlock()
	return len(m.errors) > 0
}

// Last returns the most recently added error in the collection, if any.
// Thread-safe; returns nil if the collection is empty.
func (m *MultiError) Last() error {
	m.mu.RLock()
	defer m.mu.RUnlock()
	if len(m.errors) > 0 {
		return m.errors[len(m.errors)-1]
	}
	return nil
}

// Merge combines another MultiError's errors into this one.
// Thread-safe; respects this instance’s limit and sampling settings; no-op if other is nil or empty.
func (m *MultiError) Merge(other *MultiError) {
	if other == nil || !other.Has() {
		return
	}

	other.mu.RLock()
	defer other.mu.RUnlock()

	for _, err := range other.errors {
		m.Add(err)
	}
}

// IsNull checks if the MultiError is empty or contains only null errors.
// Returns true if empty or all errors are null (via IsNull() or empty message); thread-safe.
func (m *MultiError) IsNull() bool {
	m.mu.RLock()
	defer m.mu.RUnlock()

	// Fast path for empty MultiError
	if len(m.errors) == 0 {
		return true
	}

	// Check each error for null status
	allNull := true
	for _, err := range m.errors {
		switch e := err.(type) {
		case interface{ IsNull() bool }:
			if !e.IsNull() {
				allNull = false
				break
			}
		case nil:
			continue
		default:
			if e.Error() != "" {
				allNull = false
				break
			}
		}
	}
	return allNull
}

// Single returns nil if the collection is empty, the single error if only one exists,
// or the MultiError itself if multiple errors are present.
// Thread-safe; useful for unwrapping to a single error when possible.
func (m *MultiError) Single() error {
	m.mu.RLock()
	defer m.mu.RUnlock()

	switch len(m.errors) {
	case 0:
		return nil
	case 1:
		return m.errors[0]
	default:
		return m
	}
}

// String implements the Stringer interface for a concise string representation.
// Thread-safe; delegates to Error() for formatting.
func (m *MultiError) String() string {
	return m.Error()
}

// Unwrap returns a copy of the contained errors for multi-error unwrapping.
// Implements the errors.Unwrap interface; thread-safe; returns nil if empty.
func (m *MultiError) Unwrap() []error {
	return m.Errors()
}

// WithFormatter sets a custom error formatting function.
// Returns a MultiErrorOption for use with NewMultiError; overrides default formatting.
func WithFormatter(f ErrorFormatter) MultiErrorOption {
	return func(m *MultiError) {
		m.formatter = f
	}
}

// WithLimit sets the maximum number of errors to store.
// Returns a MultiErrorOption for use with NewMultiError; 0 means unlimited, negative values are ignored.
func WithLimit(n int) MultiErrorOption {
	return func(m *MultiError) {
		if n < 0 {
			n = 0 // Ensure non-negative limit
		}
		m.limit = n
	}
}

// WithSampling enables error sampling with a specified rate (1-100).
// Returns a MultiErrorOption for use with NewMultiError; caps rate at 100 for validity.
func WithSampling(rate uint32) MultiErrorOption {
	return func(m *MultiError) {
		if rate > 100 {
			rate = 100
		}
		m.sampling = true
		m.sampleRate = rate
	}
}

// WithRand sets a custom random source for sampling, useful for testing.
// Returns a MultiErrorOption for use with NewMultiError; defaults to fastRand if nil.
func WithRand(r *rand.Rand) MultiErrorOption {
	return func(m *MultiError) {
		m.rand = r
	}
}

// MarshalJSON serializes the MultiError to JSON, including all contained errors and configuration metadata.
// Thread-safe; errors are serialized using their MarshalJSON method if available, otherwise as strings.
func (m *MultiError) MarshalJSON() ([]byte, error) {
	m.mu.RLock()
	defer m.mu.RUnlock()

	// Get buffer from pool for efficiency
	buf := jsonBufferPool.Get().(*bytes.Buffer)
	defer jsonBufferPool.Put(buf)
	buf.Reset()

	// Create encoder
	enc := json.NewEncoder(buf)
	enc.SetEscapeHTML(false)

	// Define JSON structure
	type jsonError struct {
		Error interface{} `json:"error"` // Holds either JSON-marshaled error or string
	}

	je := struct {
		Count      int         `json:"count"`                 // Number of errors
		Limit      int         `json:"limit,omitempty"`       // Maximum error limit (omitted if 0)
		Sampling   bool        `json:"sampling,omitempty"`    // Whether sampling is enabled
		SampleRate uint32      `json:"sample_rate,omitempty"` // Sampling rate (1-100, omitted if not sampling)
		Errors     []jsonError `json:"errors"`                // List of errors
	}{
		Count:      len(m.errors),
		Limit:      m.limit,
		Sampling:   m.sampling,
		SampleRate: m.sampleRate,
	}

	// Serialize each error
	je.Errors = make([]jsonError, len(m.errors))
	for i, err := range m.errors {
		if err == nil {
			je.Errors[i] = jsonError{Error: nil}
			continue
		}
		// Check if the error implements json.Marshaler
		if marshaler, ok := err.(json.Marshaler); ok {
			marshaled, err := marshaler.MarshalJSON()
			if err != nil {
				// Fallback to string if marshaling fails
				je.Errors[i] = jsonError{Error: err.Error()}
			} else {
				var raw json.RawMessage = marshaled
				je.Errors[i] = jsonError{Error: raw}
			}
		} else {
			// Use error string for non-marshaler errors
			je.Errors[i] = jsonError{Error: err.Error()}
		}
	}

	// Encode JSON
	if err := enc.Encode(je); err != nil {
		return nil, fmt.Errorf("failed to marshal MultiError: %v", err)
	}

	// Remove trailing newline
	result := buf.Bytes()
	if len(result) > 0 && result[len(result)-1] == '\n' {
		result = result[:len(result)-1]
	}
	return result, nil
}

// defaultFormat provides the default formatting for multiple errors.
// Returns a semicolon-separated list prefixed with the error count (e.g., "errors(3): err1; err2; err3").
func defaultFormat(errs []error) string {
	var sb strings.Builder
	sb.WriteString(fmt.Sprintf("errors(%d): ", len(errs)))
	for i, err := range errs {
		if i > 0 {
			sb.WriteString("; ")
		}
		sb.WriteString(err.Error())
	}
	return sb.String()
}

// fastRand generates a quick pseudo-random number for sampling.
// Uses a simple xorshift algorithm based on the current time; not cryptographically secure.
var fastRandState uint32 = 1 // Must be non-zero

func fastRand() uint32 {
	for {
		// Atomically load the current state
		old := atomic.LoadUint32(&fastRandState)
		// Xorshift computation
		x := old
		x ^= x << 13
		x ^= x >> 17
		x ^= x << 5
		// Attempt to store the new state atomically
		if atomic.CompareAndSwapUint32(&fastRandState, old, x) {
			return x
		}
		// Otherwise retry
	}
}