mirror of
https://github.com/owncloud/ocis
synced 2026-04-25 17:25:21 +02:00
* chore(deps): bump github.com/olekukonko/tablewriter from 0.0.5 to 1.0.9 Bumps [github.com/olekukonko/tablewriter](https://github.com/olekukonko/tablewriter) from 0.0.5 to 1.0.9. - [Commits](https://github.com/olekukonko/tablewriter/compare/v0.0.5...v1.0.9) --- updated-dependencies: - dependency-name: github.com/olekukonko/tablewriter dependency-version: 1.0.9 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> * fix: adjust to tablewriter 1.0.9 (AI-powered) Signed-off-by: Julian Koberg <julian.koberg@kiteworks.com> * update to NewTable * bring back a Footer * fix the tests --------- Signed-off-by: dependabot[bot] <support@github.com> Signed-off-by: Julian Koberg <julian.koberg@kiteworks.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Julian Koberg <julian.koberg@kiteworks.com> Co-authored-by: Roman Perekhod <rperekhod@owncloud.com>
424 lines
11 KiB
Go
424 lines
11 KiB
Go
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
|
||
}
|
||
}
|