diff --git a/go.mod b/go.mod index dd49fa31..82bde83e 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/iancoleman/strcase v0.3.0 github.com/launchdarkly/json-patch v0.0.0-20180720210516-dd68d883319f github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 - github.com/olekukonko/tablewriter v1.0.8 + github.com/olekukonko/tablewriter v1.0.9 github.com/petar-dambovaliev/aho-corasick v0.0.0-20211021192214-5ab2d9280aa9 github.com/spf13/cobra v1.8.1 github.com/spf13/pflag v1.0.7 @@ -46,8 +46,8 @@ require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect - github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6 // indirect - github.com/olekukonko/ll v0.0.8 // indirect + github.com/olekukonko/errors v1.1.0 // indirect + github.com/olekukonko/ll v0.0.9 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/pjbgf/sha1cd v0.3.2 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect diff --git a/go.sum b/go.sum index 43d9d857..5232d751 100644 --- a/go.sum +++ b/go.sum @@ -179,12 +179,12 @@ github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6T github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= -github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6 h1:r3FaAI0NZK3hSmtTDrBVREhKULp8oUeqLT5Eyl2mSPo= -github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= -github.com/olekukonko/ll v0.0.8 h1:sbGZ1Fx4QxJXEqL/6IG8GEFnYojUSQ45dJVwN2FH2fc= -github.com/olekukonko/ll v0.0.8/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g= -github.com/olekukonko/tablewriter v1.0.8 h1:f6wJzHg4QUtJdvrVPKco4QTrAylgaU0+b9br/lJxEiQ= -github.com/olekukonko/tablewriter v1.0.8/go.mod h1:H428M+HzoUXC6JU2Abj9IT9ooRmdq9CxuDmKMtrOCMs= +github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM= +github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= +github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI= +github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g= +github.com/olekukonko/tablewriter v1.0.9 h1:XGwRsYLC2bY7bNd93Dk51bcPZksWZmLYuaTHR0FqfL8= +github.com/olekukonko/tablewriter v1.0.9/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo= github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= diff --git a/vendor/github.com/olekukonko/errors/README.md b/vendor/github.com/olekukonko/errors/README.md index e99cf999..8830486e 100644 --- a/vendor/github.com/olekukonko/errors/README.md +++ b/vendor/github.com/olekukonko/errors/README.md @@ -48,6 +48,11 @@ go get github.com/olekukonko/errors@latest --- +> [!NOTE] +> ✓ added support for `errors.Errorf("user %w not found", errors.New("bob"))` +> ✓ added support for `sequential chain` execution +`` + ## Using the `errors` Package ### Basic Error Creation @@ -74,6 +79,7 @@ func main() { #### Formatted Error ```go +// main.go package main import ( @@ -83,12 +89,21 @@ import ( func main() { // Formatted error without stack trace - err := errors.Newf("user %s not found", "bob") - fmt.Println(err) // Output: "user bob not found" + errNoWrap := errors.Newf("user %s not found", "bob") + fmt.Println(errNoWrap) // Output: "user bob not found" + + // Standard formatted error, no fmt.Errorf needed (using own pkg) + stdErrNoWrap := errors.Stdf("user %s not found", "bob") + fmt.Println(stdErrNoWrap) // Output: "user bob not found" - // Standard formatted error, no fmt.Errorf needed - stdErr := errors.Stdf("user %s not found", "bob") - fmt.Println(stdErr) // Output: "user bob not found" + // Added support for %w (compatible with fmt.Errorf output) + // errors.Errorf is alias of errors.Newf + errWrap := errors.Errorf("user %w not found", errors.New("bob")) + fmt.Println(errWrap) // Output: "user bob not found" + + // Standard formatted error for comparison + stdErrWrap := fmt.Errorf("user %w not found", fmt.Errorf("bob")) + fmt.Println(stdErrWrap) // Output: "user bob not found" } ``` @@ -243,7 +258,7 @@ func main() { enhanced := errors.WithStack(err) fmt.Println("Error with stack:") fmt.Println("Message:", enhanced.Error()) // Output: "basic error" - fmt.Println("Stack:", enhanced.Stack()) // Output: e.g., ["main.go:15", ...] + fmt.Println("Stack:", enhanced.Stack()) // Output: e.g., "main.go:15" } ``` @@ -996,6 +1011,198 @@ func contains(substr string) func(error) bool { return strings.Contains(err.Error(), substr) } } +``` + +### Chain Execution + +#### Sequential Task Processing +```go +package main + +import ( + "fmt" + "github.com/olekukonko/errors" + "time" +) + +// validateOrder checks order input. +func validateOrder() error { + return nil // Simulate successful validation +} + +// processKYC handles payment processing. +func processKYC() error { + return nil // Simulate successful validation +} + +// processPayment handles payment processing. +func processPayment() error { + return errors.New("payment declined") // Simulate payment failure +} + +// generateInvoice creates an invoice. +func generateInvoice() error { + return errors.New("invoicing unavailable") // Simulate invoicing issue +} + +// sendNotification sends a confirmation. +func sendNotification() error { + return errors.New("notification failed") // Simulate notification failure +} + +// processOrder simulates a multi-step order processing workflow. +func processOrder() error { + c := errors.NewChain() + + // Validate order input + c.Step(validateOrder).Tag("validation") + + // KYC Process + c.Step(validateOrder).Tag("validation") + + // Process payment with retries + c.Step(processPayment).Tag("billing").Retry(3, 100*time.Millisecond) + + // Generate invoice + c.Step(generateInvoice).Tag("invoicing") + + // Send notification (optional) + c.Step(sendNotification).Tag("notification").Optional() + + return c.Run() +} + +func main() { + if err := processOrder(); err != nil { + // Print error to stderr and exit + errors.Inspect(err) + } + fmt.Println("Order processed successfully") +} +``` + +#### Sequential Task Processing 2 +```go +package main + +import ( + "fmt" + "os" + + "github.com/olekukonko/errors" +) + +// validate simulates a validation check that fails. +func validate(name string) error { + return errors.Newf("validation for %s failed", name) +} + +// validateOrder checks order input. +func validateOrder() error { + return nil // Simulate successful validation +} + +// verifyKYC handles Know Your Customer verification. +func verifyKYC(name string) error { + return validate(name) // Simulate KYC validation failure +} + +// processPayment handles payment processing. +func processPayment() error { + return nil // Simulate successful payment +} + +// processOrder coordinates the order processing workflow. +func processOrder() error { + chain := errors.NewChain(). + Step(validateOrder). // Step 1: Validate order + Call(verifyKYC, "john"). // Step 2: Verify customer + Step(processPayment) // Step 3: Process payment + + if err := chain.Run(); err != nil { + return errors.Errorf("processing order: %w", err) + } + return nil +} + +func main() { + if err := processOrder(); err != nil { + // Print the full error chain to stderr + fmt.Fprintf(os.Stderr, "ERROR: %v\n", err) + // Output + // ERROR: processing order: validation for john failed + + // For debugging, you could print the stack trace: + // errors.Inspect(err) + os.Exit(1) + } + + fmt.Println("order processed successfully") +} + +``` + + +#### Retry with Timeout +```go +package main + +import ( + "context" + "fmt" + "github.com/olekukonko/errors" + "time" +) + +func main() { + c := errors.NewChain( + errors.ChainWithTimeout(1*time.Second), + ). + Step(func() error { + time.Sleep(2 * time.Second) + return errors.New("fetch failed") + }). + Tag("api"). + Retry(3, 200*time.Millisecond) + + err := c.Run() + if err != nil { + var deadlineErr error + if errors.As(err, &deadlineErr) && deadlineErr == context.DeadlineExceeded { + fmt.Println("Fetch timed out") + } else { + fmt.Printf("Fetch failed: %v\n", err) + } + return + } + fmt.Println("Fetch succeeded") +} +``` + +#### Collecting All Errors +```go +package main + +import ( + "fmt" + "github.com/olekukonko/errors" +) + +func main() { + c := errors.NewChain( + errors.ChainWithMaxErrors(2), + ). + Step(func() error { return errors.New("task 1 failed") }).Tag("task1"). + Step(func() error { return nil }).Tag("task2"). + Step(func() error { return errors.New("task 3 failed") }).Tag("task3") + + err := c.RunAll() + if err != nil { + errors.Inspect(err) + return + } + fmt.Println("All tasks completed successfully") +} ``` @@ -1326,7 +1533,7 @@ func main() { ## FAQ - **When to use `Copy()`?** - - Use `Copy()` to create a modifiable duplicate of an error without altering the original. + - Use ` SOCIALCopy()` to create a modifiable duplicate of an error without altering the original. - **When to use `Free()`?** - Use in performance-critical loops; otherwise, autofree handles it (Go 1.24+). @@ -1355,4 +1562,4 @@ func main() { - Fork, branch, commit, and PR—see [CONTRIBUTING.md](#). ## License -MIT License - See [LICENSE](LICENSE). \ No newline at end of file +MIT License - See [LICENSE](LICENSE). diff --git a/vendor/github.com/olekukonko/errors/chain.go b/vendor/github.com/olekukonko/errors/chain.go new file mode 100644 index 00000000..5dc73a58 --- /dev/null +++ b/vendor/github.com/olekukonko/errors/chain.go @@ -0,0 +1,610 @@ +package errors + +import ( + "context" + "fmt" + "log/slog" // Standard structured logging package + "reflect" + "strings" + "time" +) + +// Chain executes functions sequentially with enhanced error handling. +// Logging is optional and configured via a slog.Handler. +type Chain struct { + steps []chainStep // List of steps to execute + errors []error // Accumulated errors during execution + config chainConfig // Chain-wide configuration + lastStep *chainStep // Pointer to the last added step for configuration + logHandler slog.Handler // Optional logging handler (nil means no logging) + cancel context.CancelFunc // Function to cancel the context +} + +// chainStep represents a single step in the chain. +type chainStep struct { + execute func() error // Function to execute for this step + optional bool // If true, errors don't stop the chain + config stepConfig // Step-specific configuration +} + +// chainConfig holds chain-wide settings. +type chainConfig struct { + timeout time.Duration // Maximum duration for the entire chain + maxErrors int // Maximum number of errors before stopping (-1 for unlimited) + autoWrap bool // Whether to automatically wrap errors with additional context +} + +// stepConfig holds configuration for an individual step. +type stepConfig struct { + context map[string]interface{} // Arbitrary key-value pairs for context + category ErrorCategory // Category for error classification + code int // Numeric error code + retry *Retry // Retry policy for the step + logOnFail bool // Whether to log errors automatically + metricsLabel string // Label for metrics (not used in this code) + logAttrs []slog.Attr // Additional attributes for logging +} + +// ChainOption defines a function that configures a Chain. +type ChainOption func(*Chain) + +// NewChain creates a new Chain with the given options. +// Logging is disabled by default (logHandler is nil). +func NewChain(opts ...ChainOption) *Chain { + c := &Chain{ + config: chainConfig{ + autoWrap: true, // Enable error wrapping by default + maxErrors: -1, // No limit on errors by default + }, + // logHandler is nil, meaning no logging unless explicitly configured + } + // Apply each configuration option + for _, opt := range opts { + opt(c) + } + return c +} + +// ChainWithLogHandler sets a custom slog.Handler for logging. +// If handler is nil, logging is effectively disabled. +func ChainWithLogHandler(handler slog.Handler) ChainOption { + return func(c *Chain) { + c.logHandler = handler + } +} + +// ChainWithTimeout sets a timeout for the entire chain. +func ChainWithTimeout(d time.Duration) ChainOption { + return func(c *Chain) { + c.config.timeout = d + } +} + +// ChainWithMaxErrors sets the maximum number of errors allowed. +// A value <= 0 means no limit. +func ChainWithMaxErrors(max int) ChainOption { + return func(c *Chain) { + if max <= 0 { + c.config.maxErrors = -1 // No limit + } else { + c.config.maxErrors = max + } + } +} + +// ChainWithAutoWrap enables or disables automatic error wrapping. +func ChainWithAutoWrap(auto bool) ChainOption { + return func(c *Chain) { + c.config.autoWrap = auto + } +} + +// Step adds a new step to the chain with the provided function. +// The function must return an error or nil. +func (c *Chain) Step(fn func() error) *Chain { + if fn == nil { + // Panic to enforce valid input + panic("Chain.Step: provided function cannot be nil") + } + // Create a new step with default configuration + step := chainStep{execute: fn, config: stepConfig{}} + c.steps = append(c.steps, step) + // Update lastStep to point to the newly added step + c.lastStep = &c.steps[len(c.steps)-1] + return c +} + +// Call adds a step by wrapping a function with arguments. +// It uses reflection to validate and invoke the function. +func (c *Chain) Call(fn interface{}, args ...interface{}) *Chain { + // Wrap the function and arguments into an executable step + wrappedFn, err := c.wrapCallable(fn, args...) + if err != nil { + // Panic on setup errors to catch them early + panic(fmt.Sprintf("Chain.Call setup error: %v", err)) + } + // Add the wrapped function as a step + step := chainStep{execute: wrappedFn, config: stepConfig{}} + c.steps = append(c.steps, step) + c.lastStep = &c.steps[len(c.steps)-1] + return c +} + +// Optional marks the last step as optional. +// Optional steps don't stop the chain on error. +func (c *Chain) Optional() *Chain { + if c.lastStep == nil { + // Panic if no step exists to mark as optional + panic("Chain.Optional: must call Step() or Call() before Optional()") + } + c.lastStep.optional = true + return c +} + +// WithLog adds logging attributes to the last step. +func (c *Chain) WithLog(attrs ...slog.Attr) *Chain { + if c.lastStep == nil { + // Panic if no step exists to configure + panic("Chain.WithLog: must call Step() or Call() before WithLog()") + } + // Append attributes to the step's logging configuration + c.lastStep.config.logAttrs = append(c.lastStep.config.logAttrs, attrs...) + return c +} + +// Timeout sets a timeout for the entire chain. +func (c *Chain) Timeout(d time.Duration) *Chain { + c.config.timeout = d + return c +} + +// MaxErrors sets the maximum number of errors allowed. +func (c *Chain) MaxErrors(max int) *Chain { + if max <= 0 { + c.config.maxErrors = -1 // No limit + } else { + c.config.maxErrors = max + } + return c +} + +// With adds a key-value pair to the last step's context. +func (c *Chain) With(key string, value interface{}) *Chain { + if c.lastStep == nil { + // Panic if no step exists to configure + panic("Chain.With: must call Step() or Call() before With()") + } + // Initialize context map if nil + if c.lastStep.config.context == nil { + c.lastStep.config.context = make(map[string]interface{}) + } + // Add the key-value pair + c.lastStep.config.context[key] = value + return c +} + +// Tag sets an error category for the last step. +func (c *Chain) Tag(category ErrorCategory) *Chain { + if c.lastStep == nil { + // Panic if no step exists to configure + panic("Chain.Tag: must call Step() or Call() before Tag()") + } + c.lastStep.config.category = category + return c +} + +// Code sets a numeric error code for the last step. +func (c *Chain) Code(code int) *Chain { + if c.lastStep == nil { + // Panic if no step exists to configure + panic("Chain.Code: must call Step() or Call() before Code()") + } + c.lastStep.config.code = code + return c +} + +// Retry configures retry behavior for the last step. +// Retry configures retry behavior for the last step. +func (c *Chain) Retry(maxAttempts int, delay time.Duration, opts ...RetryOption) *Chain { + if c.lastStep == nil { + panic("Chain.Retry: must call Step() or Call() before Retry()") + } + if maxAttempts < 1 { + maxAttempts = 1 + } + + // Define default retry options + retryOpts := []RetryOption{ + WithMaxAttempts(maxAttempts), + WithDelay(delay), + WithRetryIf(func(err error) bool { return IsRetryable(err) }), + } + + // Add logging for retry attempts if a handler is configured + if c.logHandler != nil { + step := c.lastStep + retryOpts = append(retryOpts, WithOnRetry(func(attempt int, err error) { + // Prepare logging attributes + logAttrs := []slog.Attr{ + slog.Int("attempt", attempt), + slog.Int("max_attempts", maxAttempts), + } + // Enhance the error with step context + enhancedErr := c.enhanceError(err, step) + // Log the retry attempt + c.logError(enhancedErr, fmt.Sprintf("Retrying step (attempt %d/%d)", attempt, maxAttempts), step.config, logAttrs...) + })) + } + + // Append any additional retry options + retryOpts = append(retryOpts, opts...) + // Create and assign the retry configuration + c.lastStep.config.retry = NewRetry(retryOpts...) + return c +} + +// LogOnFail enables automatic logging of errors for the last step. +func (c *Chain) LogOnFail() *Chain { + if c.lastStep == nil { + // Panic if no step exists to configure + panic("Chain.LogOnFail: must call Step() or Call() before LogOnFail()") + } + c.lastStep.config.logOnFail = true + return c +} + +// Run executes the chain, stopping on the first non-optional error. +// It returns the first error encountered or nil if all steps succeed. +func (c *Chain) Run() error { + // Create a context with timeout or cancellation + ctx, cancel := c.getContextAndCancel() + defer cancel() + c.cancel = cancel + // Clear any previous errors + c.errors = c.errors[:0] + + // Execute each step in sequence + for i := range c.steps { + step := &c.steps[i] + // Check if the context has been canceled + select { + case <-ctx.Done(): + err := ctx.Err() + // Enhance the error with step context + enhancedErr := c.enhanceError(err, step) + c.errors = append(c.errors, enhancedErr) + // Log the context error + c.logError(enhancedErr, "Chain stopped due to context error before step", step.config) + return enhancedErr + default: + } + + // Execute the step + err := c.executeStep(ctx, step) + if err != nil { + // Enhance the error with step context + enhancedErr := c.enhanceError(err, step) + c.errors = append(c.errors, enhancedErr) + // Log the error if required + if step.config.logOnFail || !step.optional { + logMsg := "Chain stopped due to error in step" + if step.optional { + logMsg = "Optional step failed" + } + c.logError(enhancedErr, logMsg, step.config) + } + // Stop execution if the step is not optional + if !step.optional { + return enhancedErr + } + } + } + // Return nil if all steps completed successfully + return nil +} + +// RunAll executes all steps, collecting errors without stopping. +// It returns a MultiError containing all errors or nil if none occurred. +func (c *Chain) RunAll() error { + ctx, cancel := c.getContextAndCancel() + defer cancel() + c.cancel = cancel + c.errors = c.errors[:0] + multi := NewMultiError() + + for i := range c.steps { + step := &c.steps[i] + select { + case <-ctx.Done(): + err := ctx.Err() + enhancedErr := c.enhanceError(err, step) + c.errors = append(c.errors, enhancedErr) + multi.Add(enhancedErr) + c.logError(enhancedErr, "Chain stopped due to context error before step (RunAll)", step.config) + goto endRunAll + default: + } + + err := c.executeStep(ctx, step) + if err != nil { + enhancedErr := c.enhanceError(err, step) + c.errors = append(c.errors, enhancedErr) + multi.Add(enhancedErr) + if step.config.logOnFail && c.logHandler != nil { + c.logError(enhancedErr, "Step failed during RunAll", step.config) + } + if c.config.maxErrors > 0 && multi.Count() >= c.config.maxErrors { + if c.logHandler != nil { + // Create a logger to log the max errors condition + logger := slog.New(c.logHandler) + logger.LogAttrs( + context.Background(), + slog.LevelError, + fmt.Sprintf("Stopping RunAll after reaching max errors (%d)", c.config.maxErrors), + slog.Int("max_errors", c.config.maxErrors), + ) + } + goto endRunAll + } + } + } + +endRunAll: + return multi.Single() +} + +// Errors returns a copy of the collected errors. +func (c *Chain) Errors() []error { + if len(c.errors) == 0 { + return nil + } + // Create a copy to prevent external modification + errs := make([]error, len(c.errors)) + copy(errs, c.errors) + return errs +} + +// Len returns the number of steps in the chain. +func (c *Chain) Len() int { + return len(c.steps) +} + +// HasErrors checks if any errors were collected. +func (c *Chain) HasErrors() bool { + return len(c.errors) > 0 +} + +// LastError returns the most recent error or nil if none exist. +func (c *Chain) LastError() error { + if len(c.errors) > 0 { + return c.errors[len(c.errors)-1] + } + return nil +} + +// Reset clears the chain's steps, errors, and context. +func (c *Chain) Reset() { + if c.cancel != nil { + // Cancel any active context + c.cancel() + c.cancel = nil + } + // Clear steps and errors + c.steps = c.steps[:0] + c.errors = c.errors[:0] + c.lastStep = nil +} + +// Unwrap returns the collected errors (alias for Errors). +func (c *Chain) Unwrap() []error { + return c.errors +} + +// getContextAndCancel creates a context based on the chain's timeout. +// It returns a context and its cancellation function. +func (c *Chain) getContextAndCancel() (context.Context, context.CancelFunc) { + parentCtx := context.Background() + if c.config.timeout > 0 { + // Create a context with a timeout + return context.WithTimeout(parentCtx, c.config.timeout) + } + // Create a cancellable context + return context.WithCancel(parentCtx) +} + +// logError logs an error with step-specific context and attributes. +// It only logs if a handler is configured and the error is non-nil. +func (c *Chain) logError(err error, msg string, config stepConfig, additionalAttrs ...slog.Attr) { + // Skip logging if no handler is set or error is nil + if c == nil || c.logHandler == nil || err == nil { + return + } + + // Create a logger on demand using the configured handler + logger := slog.New(c.logHandler) + + // Initialize attributes with error and timestamp + allAttrs := make([]slog.Attr, 0, 5+len(config.logAttrs)+len(additionalAttrs)) + allAttrs = append(allAttrs, slog.Any("error", err)) + allAttrs = append(allAttrs, slog.Time("timestamp", time.Now())) + + // Add step-specific metadata + if config.category != "" { + allAttrs = append(allAttrs, slog.String("category", string(config.category))) + } + if config.code != 0 { + allAttrs = append(allAttrs, slog.Int("code", config.code)) + } + for k, v := range config.context { + allAttrs = append(allAttrs, slog.Any(k, v)) + } + allAttrs = append(allAttrs, config.logAttrs...) + allAttrs = append(allAttrs, additionalAttrs...) + + // Add stack trace and error name if the error is of type *Error + if e, ok := err.(*Error); ok { + if stack := e.Stack(); len(stack) > 0 { + // Format stack trace, truncating if too long + stackStr := "\n\t" + strings.Join(stack, "\n\t") + if len(stackStr) > 1000 { + stackStr = stackStr[:1000] + "..." + } + allAttrs = append(allAttrs, slog.String("stacktrace", stackStr)) + } + if name := e.Name(); name != "" { + allAttrs = append(allAttrs, slog.String("error_name", name)) + } + } + + // Log the error at ERROR level with all attributes + // Use a defer to catch any panics during logging + defer func() { + if r := recover(); r != nil { + // Print to stdout to avoid infinite recursion + fmt.Printf("ERROR: Recovered from panic during logging: %v\nAttributes: %v\n", r, allAttrs) + } + }() + logger.LogAttrs(context.Background(), slog.LevelError, msg, allAttrs...) +} + +// wrapCallable wraps a function and its arguments into an executable step. +// It uses reflection to validate the function and arguments. +func (c *Chain) wrapCallable(fn interface{}, args ...interface{}) (func() error, error) { + val := reflect.ValueOf(fn) + typ := val.Type() + + // Ensure the provided value is a function + if typ.Kind() != reflect.Func { + return nil, fmt.Errorf("provided 'fn' is not a function (got %T)", fn) + } + // Check if the number of arguments matches the function's signature + if typ.NumIn() != len(args) { + return nil, fmt.Errorf("function expects %d arguments, but %d were provided", typ.NumIn(), len(args)) + } + + // Prepare argument values + argVals := make([]reflect.Value, len(args)) + errorType := reflect.TypeOf((*error)(nil)).Elem() + for i, arg := range args { + expectedType := typ.In(i) + var providedVal reflect.Value + if arg != nil { + providedVal = reflect.ValueOf(arg) + // Check if the argument type is assignable to the expected type + if !providedVal.Type().AssignableTo(expectedType) { + // Special case for error interfaces + if expectedType.Kind() == reflect.Interface && expectedType.Implements(errorType) && providedVal.Type().Implements(errorType) { + // Allow error interface + } else { + return nil, fmt.Errorf("argument %d type mismatch: expected %s, got %s", i, expectedType, providedVal.Type()) + } + } + } else { + // Handle nil arguments for nullable types + switch expectedType.Kind() { + case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice: + providedVal = reflect.Zero(expectedType) + default: + return nil, fmt.Errorf("argument %d is nil, but expected non-nillable type %s", i, expectedType) + } + } + argVals[i] = providedVal + } + + // Validate the function's return type + if typ.NumOut() > 1 || (typ.NumOut() == 1 && !typ.Out(0).Implements(errorType)) { + return nil, fmt.Errorf("function must return either no values or a single error (got %d return values)", typ.NumOut()) + } + + // Return a wrapped function that calls the original with the provided arguments + return func() error { + results := val.Call(argVals) + if len(results) == 1 && results[0].Interface() != nil { + return results[0].Interface().(error) + } + return nil + }, nil +} + +// executeStep runs a single step, applying retries if configured. +func (c *Chain) executeStep(ctx context.Context, step *chainStep) error { + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + if step.config.retry != nil { + retry := step.config.retry.Transform(WithContext(ctx)) + // Wrap step execution to respect context + wrappedFn := func() error { + type result struct { + err error + } + done := make(chan result, 1) + go func() { + done <- result{err: step.execute()} + }() + select { + case res := <-done: + return res.err + case <-ctx.Done(): + return ctx.Err() + } + } + return retry.Execute(wrappedFn) + } + // Non-retry case also respects context + type result struct { + err error + } + done := make(chan result, 1) + go func() { + done <- result{err: step.execute()} + }() + select { + case res := <-done: + return res.err + case <-ctx.Done(): + return ctx.Err() + } +} + +// enhanceError wraps an error with additional context from the step. +func (c *Chain) enhanceError(err error, step *chainStep) error { + if err == nil || !c.config.autoWrap { + // Return the error unchanged if nil or autoWrap is disabled + return err + } + + // Initialize the base error + var baseError *Error + if e, ok := err.(*Error); ok { + // Copy existing *Error to preserve its properties + baseError = e.Copy() + } else { + // Create a new *Error wrapping the original + baseError = New(err.Error()).Wrap(err).WithStack() + } + + if step != nil { + // Add step-specific context to the error + if step.config.category != "" && baseError.Category() == "" { + baseError.WithCategory(step.config.category) + } + if step.config.code != 0 && baseError.Code() == 0 { + baseError.WithCode(step.config.code) + } + for k, v := range step.config.context { + baseError.With(k, v) + } + for _, attr := range step.config.logAttrs { + baseError.With(attr.Key, attr.Value.Any()) + } + if step.config.retry != nil && !baseError.HasContextKey(ctxRetry) { + // Mark the error as retryable if retries are configured + baseError.WithRetryable() + } + } + + return baseError +} diff --git a/vendor/github.com/olekukonko/errors/errors.go b/vendor/github.com/olekukonko/errors/errors.go index 07d1e3d1..4f6509da 100644 --- a/vendor/github.com/olekukonko/errors/errors.go +++ b/vendor/github.com/olekukonko/errors/errors.go @@ -1,3 +1,9 @@ +// Package errors provides a robust error handling library with support for +// error wrapping, stack traces, context storage, and retry mechanisms. It extends +// the standard library's error interface with features like HTTP-like status codes, +// error categorization, and JSON serialization, while maintaining compatibility +// with `errors.Is`, `errors.As`, and `errors.Unwrap`. The package is thread-safe +// and optimized with object pooling for performance. package errors import ( @@ -5,39 +11,49 @@ import ( "encoding/json" "errors" "fmt" + "regexp" "runtime" "strings" "sync" "sync/atomic" ) +// Constants defining default configuration and context keys. const ( - ctxTimeout = "[error] timeout" // Context key for marking timeout errors - ctxRetry = "[error] retry" // Context key for marking retryable errors + ctxTimeout = "[error] timeout" // Context key marking timeout errors. + ctxRetry = "[error] retry" // Context key marking retryable errors. - contextSize = 4 // Default size of fixed-size context array - bufferSize = 256 // Initial buffer size for JSON marshaling - warmUpSize = 100 // Number of errors to pre-warm the pool - stackDepth = 32 // Default maximum stack trace depth + contextSize = 4 // Initial size of fixed-size context array for small contexts. + bufferSize = 256 // Initial buffer size for JSON marshaling. + warmUpSize = 100 // Number of errors to pre-warm the pool for efficiency. + stackDepth = 32 // Maximum stack trace depth to prevent excessive memory use. + + DefaultCode = 500 // Default HTTP status code for errors if not specified. ) +// spaceRe is a precompiled regex for normalizing whitespace in error messages. +var spaceRe = regexp.MustCompile(`\s+`) + +// ErrorCategory is a string type for categorizing errors (e.g., "network", "validation"). type ErrorCategory string // ErrorOpts provides options for customizing error creation. type ErrorOpts struct { - SkipStack int // Number of stack frames to skip when capturing the stack trace + SkipStack int // Number of stack frames to skip when capturing the stack trace. } -// Config defines the configuration for the errors package. +// Config defines the global configuration for the errors package, controlling +// stack depth, context size, pooling, and frame filtering. type Config struct { - StackDepth int // Maximum depth of the stack trace; 0 uses default - ContextSize int // Initial size of the context map; 0 uses default - DisablePooling bool // Disables object pooling for errors if true - FilterInternal bool // Filters internal package frames from stack traces if true - AutoFree bool // Automatically frees errors to pool if true + StackDepth int // Maximum stack trace depth; 0 uses default (32). + ContextSize int // Initial context map size; 0 uses default (4). + DisablePooling bool // If true, disables object pooling for errors. + FilterInternal bool // If true, filters internal package frames from stack traces. + AutoFree bool // If true, automatically frees errors to pool after use. } -// cachedConfig holds the current configuration, updated only on Configure(). +// cachedConfig holds the current configuration, updated only by Configure(). +// Protected by configMu for thread-safety. type cachedConfig struct { stackDepth int contextSize int @@ -47,14 +63,19 @@ type cachedConfig struct { } var ( + // currentConfig stores the active configuration, read frequently and updated rarely. currentConfig cachedConfig - configMu sync.RWMutex - errorPool = NewErrorPool() // Custom pool for Error instances - stackPool = sync.Pool{ // Pool for stack trace slices + // configMu protects updates to currentConfig for thread-safety. + configMu sync.RWMutex + // errorPool manages reusable Error instances to reduce allocations. + errorPool = NewErrorPool() + // stackPool manages reusable stack trace slices for efficiency. + stackPool = sync.Pool{ New: func() interface{} { return make([]uintptr, currentConfig.stackDepth) }, } + // emptyError is a pre-allocated empty error for lightweight reuse. emptyError = &Error{ smallContext: [contextSize]contextItem{}, msg: "", @@ -64,43 +85,42 @@ var ( } ) -//var bufferPool = sync.Pool{ -// New: func() interface{} { -// return bytes.NewBuffer(make([]byte, 0, bufferSize)) -// }, -//} - -// contextItem represents a single key-value pair in the smallContext array. +// contextItem holds a single key-value pair in the smallContext array. type contextItem struct { key string value interface{} } -// Error represents a custom error with enhanced features like context, stack traces, and wrapping. +// Error is a custom error type with enhanced features: message, name, stack trace, +// context, cause, and metadata like code and category. It is thread-safe and +// supports pooling for performance. type Error struct { - // Primary error information (most frequently accessed) - msg string // Error message - name string // Error name/type - stack []uintptr // Stack trace - - // Secondary error metadata - template string // Message template used if msg is empty - category string // Error category (e.g., "network", "validation") - count uint64 // Occurrence count for tracking frequency - code int32 // HTTP-like error code - smallCount int32 // Number of items in smallContext - - // Context and chaining - context map[string]interface{} // Additional context as key-value pairs - cause error // Wrapped underlying error - callback func() // Optional callback executed on Error() - smallContext [contextSize]contextItem // Fixed-size context storage for efficiency - - // Synchronization - mu sync.RWMutex // Protects concurrent access to mutable fields + // Primary fields (frequently accessed). + msg string // The error message displayed by Error(). + name string // The error name or type (e.g., "AuthError"). + stack []uintptr // Stack trace as program counters. + + // Secondary metadata. + template string // Fallback message template if msg is empty. + category string // Error category (e.g., "network"). + count uint64 // Occurrence count for tracking frequency. + code int32 // HTTP-like status code (e.g., 400, 500). + smallCount int32 // Number of items in smallContext. + + // Context and chaining. + context map[string]interface{} // Key-value pairs for additional context. + cause error // Wrapped underlying error for chaining. + callback func() // Optional callback invoked by Error(). + smallContext [contextSize]contextItem // Fixed-size array for small contexts. + + // Synchronization. + mu sync.RWMutex // Protects mutable fields (context, smallContext). + + // Internal flags. + formatWrapped bool // True if created by Newf with %w verb. } -// init initializes the package with default configuration and pre-warms the error pool. +// init sets up the package with default configuration and pre-warms the error pool. func init() { currentConfig = cachedConfig{ stackDepth: stackDepth, @@ -109,12 +129,15 @@ func init() { filterInternal: true, autoFree: true, } - WarmPool(warmUpSize) // Pre-warm pool with initial errors + WarmPool(warmUpSize) // Pre-allocate errors for performance. } // Configure updates the global configuration for the errors package. -// Thread-safe; should be called before heavy usage for optimal performance. -// Changes apply immediately to all subsequent error operations. +// It is thread-safe and should be called early to avoid race conditions. +// Changes apply to all subsequent error operations. +// Example: +// +// errors.Configure(errors.Config{StackDepth: 16, DisablePooling: true}) func Configure(cfg Config) { configMu.Lock() defer configMu.Unlock() @@ -130,8 +153,9 @@ func Configure(cfg Config) { currentConfig.autoFree = cfg.AutoFree } -// newError creates a new Error instance, using the pool if enabled. -// Initializes smallContext and stack appropriately. +// newError creates a new Error instance, reusing from the pool if enabled. +// Initializes smallContext and sets stack to nil. +// Internal use; prefer New, Named, or Trace for public API. func newError() *Error { if currentConfig.disablePooling { return &Error{ @@ -142,75 +166,372 @@ func newError() *Error { return errorPool.Get() } -// Empty creates a new empty error with no stack trace. -// Useful as a base for building errors incrementally. +// Empty returns a new empty error with no message, name, or stack trace. +// Useful for incrementally building errors or as a neutral base. +// Example: +// +// err := errors.Empty().With("key", "value").WithCode(400) func Empty() *Error { return emptyError } -// Named creates a new error with a specific name and stack trace. -// The name is used as the error message if no other message is set. +// Named creates an error with the specified name and captures a stack trace. +// The name doubles as the error message if no message is set. +// Use for errors where type identification and stack context are important. +// Example: +// +// err := errors.Named("AuthError").WithCode(401) func Named(name string) *Error { e := newError() e.name = name return e.WithStack() } -// New creates a fast, lightweight error without stack tracing. -// Use instead of Trace() when stack traces aren't needed for better performance. +// New creates a lightweight error with the given message and no stack trace. +// Optimized for performance; use Trace() for stack traces. +// Returns a shared empty error for empty messages to reduce allocations. +// Example: +// +// err := errors.New("invalid input") func New(text string) *Error { if text == "" { - return emptyError.Copy() // Global pre-allocated empty error + return emptyError.Copy() // Avoid modifying shared instance. } err := newError() err.msg = text return err } -// Newf is an alias to Errorf for fmt.Errorf compatibility. -// Creates a formatted error without stack traces. +// Newf creates a formatted error, supporting the %w verb for wrapping errors. +// If the format contains exactly one %w verb with a non-nil error argument, +// the error is wrapped as the cause. The final error message string generated +// by Error() will be compatible with the output of fmt.Errorf for the same inputs. +// Does not capture a stack trace by default. +// Example: +// +// cause := errors.New("db error") +// err := errors.Newf("query failed: %w", cause) +// // err.Error() will match fmt.Errorf("query failed: %w", cause).Error() +// // errors.Unwrap(err) == cause func Newf(format string, args ...interface{}) *Error { err := newError() - err.msg = fmt.Sprintf(format, args...) + + // --- Start: Parsing and Validation (mostly unchanged) --- + var wCount int + var wArgPos = -1 + var wArg error + var validationErrorMsg string + argPos := 0 + runes := []rune(format) + i := 0 + parsingOk := true + var fmtVerbs []struct { + isW bool + spec string // The full verb specifier or literal segment + argIdx int // Index in the original 'args' slice, -1 for literals/%% + } + + // Parse format string to identify verbs and literals. + for i < len(runes) && parsingOk { + segmentStart := i + if runes[i] == '%' { + if i+1 >= len(runes) { + parsingOk = false + validationErrorMsg = "ends with %" + break + } + if runes[i+1] == '%' { + fmtVerbs = append(fmtVerbs, struct { + isW bool + spec string + argIdx int + }{isW: false, spec: "%%", argIdx: -1}) + i += 2 + continue + } + i++ // Move past '%' + // Parse flags, width, precision (simplified loop) + for i < len(runes) && strings.ContainsRune("+- #0", runes[i]) { + i++ + } + for i < len(runes) && ((runes[i] >= '0' && runes[i] <= '9') || runes[i] == '.') { + i++ + } + if i >= len(runes) { + parsingOk = false + validationErrorMsg = "ends mid-specifier" + break + } + verb := runes[i] + specifierEndIndex := i + 1 + fullSpec := string(runes[segmentStart:specifierEndIndex]) + // Check if the verb consumes an argument + currentVerbConsumesArg := strings.ContainsRune("vTtbcdoqxXUeEfFgGspw", verb) + currentArgIdx := -1 + isWVerb := false + + if verb == 'w' { + isWVerb = true + wCount++ + if wCount == 1 { + wArgPos = argPos // Record position of the error argument + } else { + parsingOk = false + validationErrorMsg = "multiple %w" + break + } + } + + if currentVerbConsumesArg { + if argPos >= len(args) { + parsingOk = false + if isWVerb { // More specific message for missing %w arg + validationErrorMsg = "missing %w argument" + } else { + validationErrorMsg = fmt.Sprintf("missing argument for %s", string(verb)) + } + break + } + currentArgIdx = argPos + if isWVerb { + cause, ok := args[argPos].(error) + if !ok || cause == nil { + parsingOk = false + validationErrorMsg = "bad %w argument type" + break + } + wArg = cause // Store the actual error argument + } + argPos++ // Consume the argument position + } + fmtVerbs = append(fmtVerbs, struct { + isW bool + spec string + argIdx int + }{isW: isWVerb, spec: fullSpec, argIdx: currentArgIdx}) + i = specifierEndIndex // Move past the verb character + } else { + // Handle literal segment + literalStart := i + for i < len(runes) && runes[i] != '%' { + i++ + } + fmtVerbs = append(fmtVerbs, struct { + isW bool + spec string + argIdx int + }{isW: false, spec: string(runes[literalStart:i]), argIdx: -1}) + } + } + + // Check for too many arguments after parsing + if parsingOk && argPos < len(args) { + parsingOk = false + validationErrorMsg = fmt.Sprintf("too many arguments for format %q", format) + } + + // Handle format validation errors. + if !parsingOk { + switch validationErrorMsg { + case "multiple %w": + err.msg = fmt.Sprintf("errors.Newf: format %q has multiple %%w verbs", format) + case "missing %w argument": + err.msg = fmt.Sprintf("errors.Newf: format %q has %%w but not enough arguments", format) + case "bad %w argument type": + argValStr := "()" + if wArgPos >= 0 && wArgPos < len(args) && args[wArgPos] != nil { + argValStr = fmt.Sprintf("(%T)", args[wArgPos]) + } else if wArgPos >= len(args) { + argValStr = "(missing)" // Should be caught by "missing %w argument" case + } + err.msg = fmt.Sprintf("errors.Newf: argument %d for %%w is not a non-nil error %s", wArgPos, argValStr) + case "ends with %": + err.msg = fmt.Sprintf("errors.Newf: format %q ends with %%", format) + case "ends mid-specifier": + err.msg = fmt.Sprintf("errors.Newf: format %q ends during verb specifier", format) + default: // Includes "too many arguments" and other potential fmt issues + err.msg = fmt.Sprintf("errors.Newf: error in format %q: %s", format, validationErrorMsg) + } + err.cause = nil // Ensure no cause is set on format error + err.formatWrapped = false + return err + } + // --- End: Parsing and Validation --- + + // --- Start: Processing Valid Format String --- + if wCount == 1 && wArg != nil { + // --- Handle %w: Simulate for Sprintf and pre-format --- + err.cause = wArg // Set the cause for unwrapping + err.formatWrapped = true // Signal that msg is the final formatted string + + var finalFormat strings.Builder + var finalArgs []interface{} + causeStr := wArg.Error() // Get the string representation of the cause + + // Rebuild format string and argument list for Sprintf + for _, verb := range fmtVerbs { + if verb.isW { + // Replace the %w verb specifier (e.g., "%w", "%+w") with "%s" + finalFormat.WriteString("%s") + // Add the cause's *string* to the arguments list for the new %s + finalArgs = append(finalArgs, causeStr) + } else { + // Keep the original literal segment or non-%w verb specifier + finalFormat.WriteString(verb.spec) + if verb.argIdx != -1 { + // Add the original argument for this non-%w verb/literal + finalArgs = append(finalArgs, args[verb.argIdx]) + } + } + } + + // Format using the *modified* format string and arguments list + result, fmtErr := FmtErrorCheck(finalFormat.String(), finalArgs...) + if fmtErr != nil { + // Handle potential errors during the final formatting step + // This is unlikely if parsing passed, but possible with complex verbs/args + err.msg = fmt.Sprintf("errors.Newf: formatting error during %%w simulation for format %q: %v", format, fmtErr) + err.cause = nil // Don't keep the cause if final formatting failed + err.formatWrapped = false + } else { + // Store the final, fully formatted string, matching fmt.Errorf output + err.msg = result + } + // --- End %w Simulation --- + + } else { + // --- No %w or wArg is nil: Format directly (original logic) --- + result, fmtErr := FmtErrorCheck(format, args...) + if fmtErr != nil { + err.msg = fmt.Sprintf("errors.Newf: formatting error for format %q: %v", format, fmtErr) + err.cause = nil + err.formatWrapped = false + } else { + err.msg = result + err.formatWrapped = false // Ensure false if no %w was involved + } + } + // --- End: Processing Valid Format String --- + return err } -// Std creates a standard error using errors.New, provided for backward compatibility. -// This function serves as a lightweight wrapper around the standard library's error creation, -// allowing users to opt into basic error handling without adopting the full features of this package. +// Errorf is an alias for Newf, providing a familiar interface compatible with +// fmt.Errorf. It creates a formatted error without capturing a stack trace. +// See Newf for full details on formatting, including %w support for error wrapping. +// +// Example: +// +// err := errors.Errorf("failed: %w", errors.New("cause")) +// // err.Error() == "failed: cause" +func Errorf(format string, args ...interface{}) *Error { + return Newf(format, args...) +} + +// FmtErrorCheck safely formats a string using fmt.Sprintf, catching panics. +// Returns the formatted string and any error encountered. +// Internal use by Newf to validate format strings. +// Example: +// +// result, err := FmtErrorCheck("value: %s", "test") +func FmtErrorCheck(format string, args ...interface{}) (result string, err error) { + defer func() { + if r := recover(); r != nil { + if e, ok := r.(error); ok { + err = e + } else { + err = fmt.Errorf("panic during formatting: %v", r) + } + } + }() + result = fmt.Sprintf(format, args...) + return result, nil +} + +// countFmtArgs counts format specifiers that consume arguments in a format string. +// Ignores %% and non-consuming verbs like %n. +// Internal use by Newf for argument validation. +func countFmtArgs(format string) int { + count := 0 + runes := []rune(format) + i := 0 + for i < len(runes) { + if runes[i] == '%' { + if i+1 < len(runes) && runes[i+1] == '%' { + i += 2 // Skip %% + continue + } + i++ // Move past % + for i < len(runes) && (runes[i] == '+' || runes[i] == '-' || runes[i] == '#' || + runes[i] == ' ' || runes[i] == '0' || + (runes[i] >= '1' && runes[i] <= '9') || runes[i] == '.') { + i++ + } + if i < len(runes) { + if strings.ContainsRune("vTtbcdoqxXUeEfFgGsp", runes[i]) { + count++ + } + i++ // Move past verb + } + } else { + i++ + } + } + return count +} + +// Std creates a standard error using errors.New for compatibility. +// Does not capture stack traces or add context. +// Example: +// +// err := errors.Std("simple error") func Std(text string) error { return errors.New(text) } -// Stdf creates a formatted standard error using fmt.Errorf, provided for backward compatibility. -// This function wraps the standard library's formatted error creation, offering a simple alternative -// to the package's enhanced error handling while maintaining compatibility with existing codebases. +// Stdf creates a formatted standard error using fmt.Errorf for compatibility. +// Supports %w for wrapping; does not capture stack traces. +// Example: +// +// err := errors.Stdf("failed: %w", cause) func Stdf(format string, a ...interface{}) error { return fmt.Errorf(format, a...) } -// Trace creates an error with stack trace capture enabled. -// Use when call stacks are needed for debugging; has performance overhead. +// Trace creates an error with the given message and captures a stack trace. +// Use when debugging context is needed; for performance, prefer New(). +// Example: +// +// err := errors.Trace("operation failed") func Trace(text string) *Error { e := New(text) return e.WithStack() } -// Tracef creates a formatted error with stack trace. -// Combines Errorf and WithStack for convenience. +// Tracef creates a formatted error with a stack trace. +// Supports %w for wrapping errors. +// Example: +// +// err := errors.Tracef("query %s failed: %w", query, cause) func Tracef(format string, args ...interface{}) *Error { e := Newf(format, args...) return e.WithStack() } // As attempts to assign the error or one in its chain to the target interface. -// It only assigns when the target is a **Error and the current error node has a non-empty name. -// If the current node has an empty name, it delegates to its wrapped cause. +// Supports *Error and standard error types, traversing the cause chain. +// Returns true if successful. +// Example: +// +// var target *Error +// if errors.As(err, &target) { +// fmt.Println(target.Name()) +// } func (e *Error) As(target interface{}) bool { if e == nil { return false } - // Handle *Error target (for stderrors.As compatibility) + // Handle *Error target. if targetPtr, ok := target.(*Error); ok { current := e for current != nil { @@ -228,7 +549,7 @@ func (e *Error) As(target interface{}) bool { } return false } - // Handle *error target - unwrap to innermost error + // Handle *error target. if targetErr, ok := target.(*error); ok { innermost := error(e) current := error(e) @@ -243,8 +564,7 @@ func (e *Error) As(target interface{}) bool { *targetErr = innermost return true } - - // Delegate to cause for other types + // Delegate to cause for other types. if e.cause != nil { return errors.As(e.cause, target) } @@ -252,26 +572,44 @@ func (e *Error) As(target interface{}) bool { } // Callback sets a function to be called when Error() is invoked. -// Useful for logging or side effects; returns the error for chaining. +// Useful for logging or side effects on error access. +// Example: +// +// err := errors.New("test").Callback(func() { log.Println("error accessed") }) func (e *Error) Callback(fn func()) *Error { e.callback = fn return e } -// Category returns the error's category, if set. -// Returns an empty string if no category is defined. +// Category returns the error’s category, if set. +// Example: +// +// if err.Category() == "network" { +// handleNetworkError(err) +// } func (e *Error) Category() string { return e.category } -// Code returns the error's status code, if set. -// Returns 0 if no code is defined. +// Code returns the error’s HTTP-like status code, if set. +// Returns 0 if no code is set. +// Example: +// +// if err.Code() == 404 { +// renderNotFound() +// } func (e *Error) Code() int { return int(e.code) } -// Context returns the error's context as a map. -// Converts smallContext to a map if needed; returns nil if empty. +// Context returns the error’s context as a map, merging smallContext and map-based context. +// Thread-safe; lazily initializes the map if needed. +// Example: +// +// ctx := err.Context() +// if userID, ok := ctx["user_id"]; ok { +// fmt.Println(userID) +// } func (e *Error) Context() map[string]interface{} { e.mu.RLock() defer e.mu.RUnlock() @@ -285,8 +623,11 @@ func (e *Error) Context() map[string]interface{} { return e.context } -// Copy creates a deep copy of the error, preserving all fields except stack. -// The new error does not capture a new stack trace unless explicitly added. +// Copy creates a deep copy of the error, preserving all fields except stack freshness. +// The new error can be modified independently. +// Example: +// +// newErr := err.Copy().With("new_key", "value") func (e *Error) Copy() *Error { if e == emptyError { return &Error{ @@ -327,56 +668,75 @@ func (e *Error) Copy() *Error { } // Count returns the number of times the error has been incremented. -// Useful for tracking occurrence frequency. +// Useful for tracking error frequency. +// Example: +// +// fmt.Printf("Error occurred %d times", err.Count()) func (e *Error) Count() uint64 { return e.count } // Err returns the error as an error interface. -// Provided for compatibility; simply returns the error itself. +// Useful for type assertions or interface compatibility. +// Example: +// +// var stdErr error = err.Err() func (e *Error) Err() error { return e } // Error returns the string representation of the error. -// Prioritizes msg, then template, then name, falling back to "unknown error". -// Executes callback if set before returning the message. +// If the error was created using Newf/Errorf with the %w verb, it returns the +// pre-formatted string compatible with fmt.Errorf. +// Otherwise, it combines the message, template, or name with the cause's error +// string, separated by ": ". Invokes any set callback. func (e *Error) Error() string { if e.callback != nil { e.callback() } - var msg string - switch { - case e.msg != "": - msg = e.msg - case e.template != "": - msg = e.template - case e.name != "": - msg = e.name - default: - msg = "unknown error" + + // If created by Newf/Errorf with %w, msg already contains the final string. + if e.formatWrapped { + return e.msg // Return the pre-formatted fmt.Errorf-compatible string } + + // --- Original logic for errors not created via Newf("%w", ...) --- + // --- or errors created via New/Named and then Wrap() called. --- + var buf strings.Builder + + // Append primary message part (msg, template, or name) + if e.msg != "" { + buf.WriteString(e.msg) + } else if e.template != "" { + buf.WriteString(e.template) + } else if e.name != "" { + buf.WriteString(e.name) + } + + // Append cause if it exists (only relevant if not formatWrapped) if e.cause != nil { - causeMsg := e.cause.Error() - if msg != "" && causeMsg != "" { - msg = msg + ": " + causeMsg - } else if causeMsg != "" { - msg = causeMsg + if buf.Len() > 0 { + // Add separator only if there was a prefix message/name/template + buf.WriteString(": ") } + buf.WriteString(e.cause.Error()) + } else if buf.Len() == 0 { + // Handle case where msg/template/name are empty AND cause is nil + // Could return a specific string like "[empty error]" or just "" + return "" // Return empty string for a truly empty error } - return msg -} -// Errorf creates a formatted error without stack traces. -// Compatible with fmt.Errorf; does not capture stack trace for performance. -func Errorf(format string, args ...interface{}) *Error { - err := newError() - err.msg = fmt.Sprintf(format, args...) - return err + return buf.String() } -// FastStack returns a lightweight stack trace without function names. -// Filters internal frames if FilterInternal is enabled; returns nil if no stack. +// FastStack returns a lightweight stack trace with file and line numbers only. +// Omits function names for performance; skips internal frames if configured. +// Returns nil if no stack trace exists. +// Example: +// +// for _, frame := range err.FastStack() { +// fmt.Println(frame) // e.g., "main.go:42" +// } func (e *Error) FastStack() []string { if e.stack == nil { return nil @@ -402,8 +762,11 @@ func (e *Error) FastStack() []string { return frames } -// Find searches the error chain for the first error matching pred. -// Starts with the current error and follows Unwrap() and Cause() chains. +// Find searches the error chain for the first error where pred returns true. +// Returns nil if no match is found or if pred is nil. +// Example: +// +// err := err.Find(func(e error) bool { return strings.Contains(e.Error(), "timeout") }) func (e *Error) Find(pred func(error) bool) error { if e == nil || pred == nil { return nil @@ -411,51 +774,73 @@ func (e *Error) Find(pred func(error) bool) error { return Find(e, pred) } -// Format returns a formatted string representation of the error. -// Includes message, code, context, and stack trace if present. +// Format returns a detailed, human-readable string representation of the error, +// including message, code, context, stack, and cause. +// Recursive for causes that are also *Error. +// Example: +// +// fmt.Println(err.Format()) +// // Output: +// // Error: failed: cause +// // Code: 500 +// // Context: +// // key: value +// // Stack: +// // 1. main.main main.go:42 func (e *Error) Format() string { var sb strings.Builder - // Error message + // Error message. sb.WriteString("Error: " + e.Error() + "\n") - // Metadata + // Metadata. if e.code != 0 { sb.WriteString(fmt.Sprintf("Code: %d\n", e.code)) } - // Context (only show context added at this level) + // Context. if ctx := e.contextAtThisLevel(); len(ctx) > 0 { sb.WriteString("Context:\n") for k, v := range ctx { - sb.WriteString(fmt.Sprintf(" %s: %v\n", k, v)) + sb.WriteString(fmt.Sprintf("\t%s: %v\n", k, v)) } } - // Stack trace + // Stack trace. if e.stack != nil { sb.WriteString("Stack:\n") for i, frame := range e.Stack() { - sb.WriteString(fmt.Sprintf(" %d. %s\n", i+1, frame)) + sb.WriteString(fmt.Sprintf("\t%d. %s\n", i+1, frame)) } } + // Cause. + if e.cause != nil { + sb.WriteString("Caused by: ") + if causeErr, ok := e.cause.(*Error); ok { + sb.WriteString(causeErr.Format()) + } else { + sb.WriteString("Error: " + e.cause.Error() + "\n") + } + sb.WriteString("\n") + } + return sb.String() } -// contextAtThisLevel returns context specific to this error level, excluding inherited context. -// Combines smallContext and context map into a single map; returns nil if empty. +// contextAtThisLevel returns context specific to this error, excluding inherited context. +// Internal use by Format to isolate context per error level. func (e *Error) contextAtThisLevel() map[string]interface{} { if e.context == nil && e.smallCount == 0 { return nil } ctx := make(map[string]interface{}) - // Add smallContext items + // Add smallContext items. for i := 0; i < int(e.smallCount); i++ { ctx[e.smallContext[i].key] = e.smallContext[i].value } - // Add map context items + // Add map context items. if e.context != nil { for k, v := range e.context { ctx[k] = v @@ -465,7 +850,11 @@ func (e *Error) contextAtThisLevel() map[string]interface{} { } // Free resets the error and returns it to the pool if pooling is enabled. -// Does nothing beyond reset if pooling is disabled. +// Safe to call multiple times; no-op if pooling is disabled. +// Call after use to prevent memory leaks when autoFree is false. +// Example: +// +// defer err.Free() func (e *Error) Free() { if currentConfig.disablePooling { return @@ -480,14 +869,24 @@ func (e *Error) Free() { errorPool.Put(e) } -// Has checks if the error contains meaningful content. -// Returns true if msg, template, name, or cause is non-empty/nil. +// Has checks if the error contains meaningful content (message, template, name, or cause). +// Returns false for nil or empty errors. +// Example: +// +// if !err.Has() { +// return nil +// } func (e *Error) Has() bool { return e != nil && (e.msg != "" || e.template != "" || e.name != "" || e.cause != nil) } -// HasContextKey checks if the specified key exists in the error's context. -// Searches both smallContext and context map; thread-safe. +// HasContextKey checks if the specified key exists in the error’s context. +// Thread-safe; checks both smallContext and map-based context. +// Example: +// +// if err.HasContextKey("user_id") { +// fmt.Println(err.Context()["user_id"]) +// } func (e *Error) HasContextKey(key string) bool { e.mu.RLock() defer e.mu.RUnlock() @@ -506,15 +905,24 @@ func (e *Error) HasContextKey(key string) bool { return false } -// Increment increases the error's count by 1 and returns the error. -// Uses atomic operation for thread safety. +// Increment atomically increases the error’s count by 1 and returns the error. +// Useful for tracking repeated occurrences. +// Example: +// +// err := err.Increment() func (e *Error) Increment() *Error { atomic.AddUint64(&e.count, 1) return e } -// Is checks if the error matches a target error by pointer equality, name, or wrapped cause. -// Ensures compatibility with stderrors.Is by prioritizing chain traversal. +// Is checks if the error matches the target by pointer, name, or cause chain. +// Compatible with errors.Is; also matches by string for standard errors. +// Returns true if the error or its cause matches the target. +// Example: +// +// if errors.Is(err, errors.New("target")) { +// handleTargetError() +// } func (e *Error) Is(target error) bool { if e == nil || target == nil { return e == target @@ -527,7 +935,7 @@ func (e *Error) Is(target error) bool { return true } } - // Add string comparison for standard errors + // Match standard errors by string. if stdErr, ok := target.(error); ok && e.Error() == stdErr.Error() { return true } @@ -537,8 +945,13 @@ func (e *Error) Is(target error) bool { return false } -// IsEmpty checks if the error has no meaningful content (empty message, no name/template/cause). -// Returns true for nil errors or errors with no data. +// IsEmpty checks if the error lacks meaningful content (no message, name, template, or cause). +// Returns true for nil or fully empty errors. +// Example: +// +// if err.IsEmpty() { +// return nil +// } func (e *Error) IsEmpty() bool { if e == nil { return true @@ -546,18 +959,23 @@ func (e *Error) IsEmpty() bool { return e.msg == "" && e.template == "" && e.name == "" && e.cause == nil } -// IsNull checks if an error is nil or represents a SQL NULL value. -// Considers both the error itself and any context values; returns true if all context is null. +// IsNull checks if the error is nil, empty, or contains only SQL NULL values in its context or cause. +// Useful for handling database-related errors. +// Example: +// +// if err.IsNull() { +// return nil +// } func (e *Error) IsNull() bool { if e == nil || e == emptyError { return true } - // If no context or cause, and no content, it's not null + // If no context or cause, and no content, it’s not null. if e.smallCount == 0 && e.context == nil && e.cause == nil { return false } - // Check cause first - if it’s null, the whole error is null + // Check cause first. if e.cause != nil { var isNull bool if ce, ok := e.cause.(*Error); ok { @@ -568,10 +986,9 @@ func (e *Error) IsNull() bool { if isNull { return true } - // If cause isn’t null, continue checking this error’s context } - // Check small context + // Check small context. if e.smallCount > 0 { allNull := true for i := 0; i < int(e.smallCount); i++ { @@ -586,7 +1003,7 @@ func (e *Error) IsNull() bool { } } - // Check regular context + // Check regular context. if e.context != nil { allNull := true for _, v := range e.context { @@ -601,10 +1018,11 @@ func (e *Error) IsNull() bool { } } - // Null if we have context and it’s all null + // Null if context exists and is all null. return e.smallCount > 0 || e.context != nil } +// jsonBufferPool manages reusable buffers for JSON marshaling to reduce allocations. var ( jsonBufferPool = sync.Pool{ New: func() interface{} { @@ -613,19 +1031,23 @@ var ( } ) -// MarshalJSON serializes the error to JSON, including name, message, context, cause, and stack. -// Handles nested *Error causes and custom marshalers efficiently. +// MarshalJSON serializes the error to JSON, including name, message, context, cause, stack, and code. +// Causes are recursively serialized if they implement json.Marshaler or are *Error. +// Example: +// +// data, _ := json.Marshal(err) +// fmt.Println(string(data)) func (e *Error) MarshalJSON() ([]byte, error) { - // Get buffer from pool + // Get buffer from pool. buf := jsonBufferPool.Get().(*bytes.Buffer) defer jsonBufferPool.Put(buf) buf.Reset() - // Create new encoder each time (no Reset available) + // Create new encoder. enc := json.NewEncoder(buf) enc.SetEscapeHTML(false) - // Prepare error data + // Prepare JSON structure. je := struct { Name string `json:"name,omitempty"` Message string `json:"message,omitempty"` @@ -639,17 +1061,17 @@ func (e *Error) MarshalJSON() ([]byte, error) { Code: e.Code(), } - // Handle context + // Add context. if ctx := e.Context(); len(ctx) > 0 { je.Context = ctx } - // Handle stack + // Add stack. if e.stack != nil { je.Stack = e.Stack() } - // Handle cause + // Add cause. if e.cause != nil { switch c := e.cause.(type) { case *Error: @@ -661,12 +1083,12 @@ func (e *Error) MarshalJSON() ([]byte, error) { } } - // Encode + // Encode JSON. if err := enc.Encode(je); err != nil { return nil, err } - // Return bytes without trailing newline + // Remove trailing newline. result := buf.Bytes() if len(result) > 0 && result[len(result)-1] == '\n' { result = result[:len(result)-1] @@ -674,21 +1096,31 @@ func (e *Error) MarshalJSON() ([]byte, error) { return result, nil } -// Msgf sets the error message using a formatted string. -// Overwrites any existing message; returns the error for chaining. +// Msgf sets the error’s message using a formatted string and returns the error. +// Overwrites any existing message. +// Example: +// +// err := err.Msgf("user %s not found", username) func (e *Error) Msgf(format string, args ...interface{}) *Error { e.msg = fmt.Sprintf(format, args...) return e } -// Name returns the error's name, if set. -// Returns an empty string if no name is defined. +// Name returns the error’s name, if set. +// Example: +// +// if err.Name() == "AuthError" { +// handleAuthError() +// } func (e *Error) Name() string { return e.name } -// Reset clears all fields of the error, preparing it for reuse. -// Does not free the stack; use Free() to return to pool. +// Reset clears all fields of the error, preparing it for reuse in the pool. +// Internal use by Free; does not release stack to stackPool. +// Example: +// +// err.Reset() // Clear all fields. func (e *Error) Reset() { e.msg = "" e.name = "" @@ -698,6 +1130,7 @@ func (e *Error) Reset() { e.count = 0 e.cause = nil e.callback = nil + e.formatWrapped = false if e.context != nil { for k := range e.context { @@ -711,8 +1144,13 @@ func (e *Error) Reset() { } } -// Stack returns a detailed stack trace as a slice of strings. -// Filters internal frames if FilterInternal is enabled; returns nil if no stack. +// Stack returns a detailed stack trace with function names, files, and line numbers. +// Filters internal frames if configured; returns nil if no stack exists. +// Example: +// +// for _, frame := range err.Stack() { +// fmt.Println(frame) // e.g., "main.main main.go:42" +// } func (e *Error) Stack() []string { if e.stack == nil { return nil @@ -742,8 +1180,11 @@ func (e *Error) Stack() []string { return trace } -// Trace ensures the error has a stack trace, capturing it if missing. -// Skips capture if stack already exists; returns the error for chaining. +// Trace ensures the error has a stack trace, capturing it if absent. +// Returns the error for chaining. +// Example: +// +// err := errors.New("failed").Trace() func (e *Error) Trace() *Error { if e.stack == nil { e.stack = captureStack(2) @@ -751,8 +1192,11 @@ func (e *Error) Trace() *Error { return e } -// Transform applies transformations to a copy of the error. -// Returns the transformed copy or the original if no changes are needed. +// Transform applies transformations to a copy of the error and returns the new error. +// The original error is unchanged; nil-safe. +// Example: +// +// newErr := err.Transform(func(e *Error) { e.With("key", "value") }) func (e *Error) Transform(fn func(*Error)) *Error { if e == nil || fn == nil { return e @@ -763,13 +1207,22 @@ func (e *Error) Transform(fn func(*Error)) *Error { } // Unwrap returns the underlying cause of the error, if any. -// Implements the errors.Unwrap interface for unwrapping chains. +// Compatible with errors.Unwrap for chain traversal. +// Example: +// +// cause := errors.Unwrap(err) func (e *Error) Unwrap() error { return e.cause } // UnwrapAll returns a slice of all errors in the chain, starting with this error. -// Traverses the cause chain, creating isolated copies of each *Error. +// Each error is isolated to prevent modifications affecting others. +// Example: +// +// chain := err.UnwrapAll() +// for _, e := range chain { +// fmt.Println(e.Error()) +// } func (e *Error) UnwrapAll() []error { if e == nil { return nil @@ -813,7 +1266,10 @@ func (e *Error) UnwrapAll() []error { } // Walk traverses the error chain, applying fn to each error. -// Starts with the current error and follows the cause chain. +// Stops if fn is nil or the chain ends. +// Example: +// +// err.Walk(func(e error) { fmt.Println(e.Error()) }) func (e *Error) Walk(fn func(error)) { if e == nil || fn == nil { return @@ -829,89 +1285,149 @@ func (e *Error) Walk(fn func(error)) { } } -// With adds a key-value pair to the error's context. -// Uses smallContext for efficiency until full, then switches to map; thread-safe. -func (e *Error) With(key string, value interface{}) *Error { - // Fast path for small context (no map needed) +// With adds key-value pairs to the error's context and returns the error. +// Uses a fixed-size array (smallContext) for up to contextSize items, then switches +// to a map. Thread-safe. Accepts variadic key-value pairs. +// Example: +// +// err := err.With("key1", value1, "key2", value2) +func (e *Error) With(keyValues ...interface{}) *Error { + if len(keyValues) == 0 { + return e + } + + // Validate that we have an even number of arguments + if len(keyValues)%2 != 0 { + keyValues = append(keyValues, "(MISSING)") + } + + // Fast path for small context when we can add all pairs to smallContext if e.smallCount < contextSize && e.context == nil { - e.mu.Lock() - // Double-check after acquiring lock - if e.smallCount < contextSize && e.context == nil { - e.smallContext[e.smallCount] = contextItem{key, value} - e.smallCount++ + remainingSlots := contextSize - int(e.smallCount) + if len(keyValues)/2 <= remainingSlots { + e.mu.Lock() + // Recheck conditions after acquiring lock + if e.smallCount < contextSize && e.context == nil { + for i := 0; i < len(keyValues); i += 2 { + key, ok := keyValues[i].(string) + if !ok { + key = fmt.Sprintf("%v", keyValues[i]) + } + e.smallContext[e.smallCount] = contextItem{key, keyValues[i+1]} + e.smallCount++ + } + e.mu.Unlock() + return e + } e.mu.Unlock() - return e } - e.mu.Unlock() } - // Slow path - requires map + // Slow path - either we have too many pairs or already using map context e.mu.Lock() defer e.mu.Unlock() + // Initialize map context if needed if e.context == nil { - e.context = make(map[string]interface{}, currentConfig.contextSize) - // Migrate existing items if any + e.context = make(map[string]interface{}, max(currentConfig.contextSize, len(keyValues)/2+int(e.smallCount))) + // Migrate existing smallContext items for i := int32(0); i < e.smallCount; i++ { e.context[e.smallContext[i].key] = e.smallContext[i].value } + // Reset smallCount since we've moved to map context + e.smallCount = 0 + } + + // Add all pairs to map context + for i := 0; i < len(keyValues); i += 2 { + key, ok := keyValues[i].(string) + if !ok { + key = fmt.Sprintf("%v", keyValues[i]) + } + e.context[key] = keyValues[i+1] } - e.context[key] = value return e } -// WithCategory sets a category for the error and returns the error. -// Useful for classifying errors (e.g., "network", "validation"). +// Helper function to get maximum of two integers +func max(a, b int) int { + if a > b { + return a + } + return b +} + +// WithCategory sets the error’s category and returns the error. +// Example: +// +// err := err.WithCategory("validation") func (e *Error) WithCategory(category ErrorCategory) *Error { e.category = string(category) return e } -// WithCode sets an HTTP-like status code for the error and returns the error. -// Overwrites any existing code. +// WithCode sets an HTTP-like status code and returns the error. +// Example: +// +// err := err.WithCode(400) func (e *Error) WithCode(code int) *Error { e.code = int32(code) return e } -// WithName sets the error's name and returns the error. -// Overwrites any existing name. +// WithName sets the error’s name and returns the error. +// Example: +// +// err := err.WithName("AuthError") func (e *Error) WithName(name string) *Error { e.name = name return e } -// WithRetryable marks the error as retryable in its context. -// Adds a "retry" key with value true; returns the error. +// WithRetryable marks the error as retryable in its context and returns the error. +// Example: +// +// err := err.WithRetryable() func (e *Error) WithRetryable() *Error { return e.With(ctxRetry, true) } -// WithStack captures the stack trace at call time and returns the error. -// Skips capturing if stack already exists or depth is 0. +// WithStack captures a stack trace if none exists and returns the error. +// Skips one frame (caller of WithStack). +// Example: +// +// err := errors.New("failed").WithStack() func (e *Error) WithStack() *Error { if e.stack == nil { - e.stack = captureStack(1) // Skip WithStack + e.stack = captureStack(1) } return e } -// WithTemplate sets a template string for the error and returns the error. -// Used as the error message if no explicit message is set. +// WithTemplate sets a message template and returns the error. +// Used as a fallback if the message is empty. +// Example: +// +// err := err.WithTemplate("operation failed") func (e *Error) WithTemplate(template string) *Error { e.template = template return e } -// WithTimeout marks the error as a timeout error in its context. -// Adds a "timeout" key with value true; returns the error. +// WithTimeout marks the error as a timeout error in its context and returns the error. +// Example: +// +// err := err.WithTimeout() func (e *Error) WithTimeout() *Error { return e.With(ctxTimeout, true) } -// Wrap associates a cause error with this error, creating an error chain. -// Returns the error for method chaining. +// Wrap associates a cause error with this error, creating a chain. +// Returns the error unchanged if cause is nil. +// Example: +// +// err := errors.New("failed").Wrap(errors.New("cause")) func (e *Error) Wrap(cause error) *Error { if cause == nil { return e @@ -920,8 +1436,23 @@ func (e *Error) Wrap(cause error) *Error { return e } -// WrapNotNil wraps a cause error only if it is non-nil. -// Returns the error for method chaining; no-op if cause is nil. +// Wrapf wraps a cause error with formatted message and returns the error. +// If cause is nil, returns the error unchanged. +// Example: +// +// err := errors.New("base").Wrapf(io.EOF, "read failed: %s", "file.txt") +func (e *Error) Wrapf(cause error, format string, args ...interface{}) *Error { + e.msg = fmt.Sprintf(format, args...) + if cause != nil { + e.cause = cause + } + return e +} + +// WrapNotNil wraps a cause error only if it is non-nil and returns the error. +// Example: +// +// err := err.WrapNotNil(maybeError) func (e *Error) WrapNotNil(cause error) *Error { if cause != nil { e.cause = cause @@ -929,8 +1460,12 @@ func (e *Error) WrapNotNil(cause error) *Error { return e } -// WarmPool pre-populates the error pool with a specified number of instances. -// Reduces allocation overhead during initial usage; no effect if pooling is disabled. +// WarmPool pre-populates the error pool with count instances. +// Improves performance by reducing initial allocations. +// No-op if pooling is disabled. +// Example: +// +// errors.WarmPool(1000) func WarmPool(count int) { if currentConfig.disablePooling { return @@ -945,8 +1480,12 @@ func WarmPool(count int) { } } -// WarmStackPool pre-populates the stack pool with a specified number of slices. -// Reduces allocation overhead for stack traces; no effect if pooling is disabled. +// WarmStackPool pre-populates the stack pool with count slices. +// Improves performance for stack-intensive operations. +// No-op if pooling is disabled. +// Example: +// +// errors.WarmStackPool(500) func WarmStackPool(count int) { if currentConfig.disablePooling { return diff --git a/vendor/github.com/olekukonko/errors/helper.go b/vendor/github.com/olekukonko/errors/helper.go index e023636d..06c2adc5 100644 --- a/vendor/github.com/olekukonko/errors/helper.go +++ b/vendor/github.com/olekukonko/errors/helper.go @@ -31,7 +31,7 @@ func Code(err error) int { if e, ok := err.(*Error); ok { return e.Code() } - return 500 + return DefaultCode } // Context extracts the context map from an error, if it is an *Error. @@ -63,7 +63,12 @@ func Convert(err error) *Error { } // Manual unwrapping as fallback + visited := make(map[error]bool) for unwrapped := err; unwrapped != nil; { + if visited[unwrapped] { + break // Cycle detected + } + visited[unwrapped] = true if e, ok := unwrapped.(*Error); ok { return e } @@ -229,12 +234,16 @@ func IsRetryable(err error) bool { } } } - // Fallback to context map + // Check regular context if e.context != nil { if val, ok := e.context[ctxRetry].(bool); ok { return val } } + // Check cause recursively + if e.cause != nil { + return IsRetryable(e.cause) + } } lowerMsg := strings.ToLower(err.Error()) return IsTimeout(err) || strings.Contains(lowerMsg, "retry") diff --git a/vendor/github.com/olekukonko/errors/inspect.go b/vendor/github.com/olekukonko/errors/inspect.go new file mode 100644 index 00000000..c87def9d --- /dev/null +++ b/vendor/github.com/olekukonko/errors/inspect.go @@ -0,0 +1,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 +} diff --git a/vendor/github.com/olekukonko/errors/multi_error.go b/vendor/github.com/olekukonko/errors/multi_error.go index 2419c82f..1d3dff5a 100644 --- a/vendor/github.com/olekukonko/errors/multi_error.go +++ b/vendor/github.com/olekukonko/errors/multi_error.go @@ -1,11 +1,13 @@ package errors import ( + "bytes" + "encoding/json" "fmt" "math/rand" "strings" "sync" - "time" + "sync/atomic" ) // MultiError represents a thread-safe collection of errors with enhanced features. @@ -45,40 +47,56 @@ func NewMultiError(opts ...MultiErrorOption) *MultiError { // 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(err error) { - if err == nil { +func (m *MultiError) Add(errs ...error) { + if len(errs) == 0 { return } m.mu.Lock() defer m.mu.Unlock() - // Check for duplicates by comparing error messages - for _, e := range m.errors { - if e.Error() == err.Error() { - return + for _, err := range errs { + if err == nil { + 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 + // Check for duplicates by comparing error messages + duplicate := false + for _, e := range m.errors { + if e.Error() == err.Error() { + duplicate = true + break + } } - if r > m.sampleRate { // Accept if random value is within sample rate - return + if duplicate { + continue } - } - // Respect limit if set - if m.limit > 0 && len(m.errors) >= m.limit { - return + // 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) } +} - 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. @@ -300,6 +318,75 @@ func WithRand(r *rand.Rand) MultiErrorOption { } } +// 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 { @@ -316,10 +403,21 @@ func defaultFormat(errs []error) 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 { - r := uint32(time.Now().UnixNano()) - r ^= r << 13 - r ^= r >> 17 - r ^= r << 5 - return r + 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 + } } diff --git a/vendor/github.com/olekukonko/errors/retry.go b/vendor/github.com/olekukonko/errors/retry.go index 89a73f6a..6d6df387 100644 --- a/vendor/github.com/olekukonko/errors/retry.go +++ b/vendor/github.com/olekukonko/errors/retry.go @@ -103,42 +103,124 @@ func (r *Retry) Execute(fn func() error) error { var lastErr error for attempt := 1; attempt <= r.maxAttempts; attempt++ { + // Check context before each attempt + select { + case <-r.ctx.Done(): + return r.ctx.Err() + default: + } + err := fn() if err == nil { return nil } - // Check if retry is applicable; return immediately if not retryable + lastErr = err + + // Check if we should retry if r.retryIf != nil && !r.retryIf(err) { return err } - lastErr = err if r.onRetry != nil { r.onRetry(attempt, err) } - // Exit if this was the last attempt + // Don't delay after last attempt if attempt == r.maxAttempts { break } - // Calculate delay with backoff, cap at maxDelay, and apply jitter if enabled - currentDelay := r.backoff.Backoff(attempt, r.delay) - if currentDelay > r.maxDelay { - currentDelay = r.maxDelay + // Calculate delay with backoff + delay := r.backoff.Backoff(attempt, r.delay) + if r.maxDelay > 0 && delay > r.maxDelay { + delay = r.maxDelay } if r.jitter { - currentDelay = addJitter(currentDelay) + delay = addJitter(delay) } - // Wait with respect to context cancellation or timeout + // Wait with context select { case <-r.ctx.Done(): return r.ctx.Err() + case <-time.After(delay): + } + } + + return lastErr +} + +// ExecuteContext runs the provided function with retry logic, respecting context cancellation. +// Returns nil on success or the last error if all attempts fail or context is cancelled. +func (r *Retry) ExecuteContext(ctx context.Context, fn func() error) error { + var lastErr error + + // If the retry instance already has a context, use it. Otherwise, use the provided one. + // If both are provided, maybe create a derived context? For now, prioritize the one from WithContext. + execCtx := r.ctx + if execCtx == context.Background() && ctx != nil { // Use provided ctx if retry ctx is default and provided one isn't nil + execCtx = ctx + } else if ctx == nil { // Ensure we always have a non-nil context + execCtx = context.Background() + } + // Note: This logic might need refinement depending on how contexts should interact. + // A safer approach might be: if r.ctx != background, use it. Else use provided ctx. + + for attempt := 1; attempt <= r.maxAttempts; attempt++ { + // Check context before executing the function + select { + case <-execCtx.Done(): + return execCtx.Err() // Return context error immediately + default: + // Context is okay, proceed + } + + err := fn() + if err == nil { + return nil // Success + } + + // Check if retry is applicable based on the error + if r.retryIf != nil && !r.retryIf(err) { + return err // Not retryable, return the error + } + + lastErr = err // Store the last encountered error + + // Execute the OnRetry callback if configured + if r.onRetry != nil { + r.onRetry(attempt, err) + } + + // Exit loop if this was the last attempt + if attempt == r.maxAttempts { + break + } + + // --- Calculate and apply delay --- + currentDelay := r.backoff.Backoff(attempt, r.delay) + if r.maxDelay > 0 && currentDelay > r.maxDelay { // Check maxDelay > 0 before capping + currentDelay = r.maxDelay + } + if r.jitter { + currentDelay = addJitter(currentDelay) + } + if currentDelay < 0 { // Ensure delay isn't negative after jitter + currentDelay = 0 + } + // --- Wait for the delay or context cancellation --- + select { + case <-execCtx.Done(): + // If context is cancelled during the wait, return the context error + // Often more informative than returning the last application error. + return execCtx.Err() case <-time.After(currentDelay): + // Wait finished, continue to the next attempt } } + + // All attempts failed, return the last error encountered return lastErr } diff --git a/vendor/github.com/olekukonko/ll/lh/colorized.go b/vendor/github.com/olekukonko/ll/lh/colorized.go index 3dbfed30..8f5aa53d 100644 --- a/vendor/github.com/olekukonko/ll/lh/colorized.go +++ b/vendor/github.com/olekukonko/ll/lh/colorized.go @@ -7,6 +7,7 @@ import ( "os" "sort" "strings" + "time" ) // Palette defines ANSI color codes for various log components. @@ -76,8 +77,10 @@ var lightPalette = Palette{ // writing the result to the provided writer. // Thread-safe if the underlying writer is thread-safe. type ColorizedHandler struct { - w io.Writer // Destination for colored log output - palette Palette // Color scheme for formatting + w io.Writer // Destination for colored log output + palette Palette // Color scheme for formatting + showTime bool // Whether to display timestamps + timeFormat string // Format for timestamps (defaults to time.RFC3339) } // ColorOption defines a configuration function for ColorizedHandler. @@ -104,7 +107,12 @@ func WithColorPallet(pallet Palette) ColorOption { // logger := ll.New("app").Enable().Handler(handler) // logger.Info("Test") // Output: [app] : Test func NewColorizedHandler(w io.Writer, opts ...ColorOption) *ColorizedHandler { - c := &ColorizedHandler{w: w} // Initialize with writer + // Initialize with writer + c := &ColorizedHandler{w: w, + showTime: false, + timeFormat: time.RFC3339, + } + // Apply configuration options for _, opt := range opts { opt(c) @@ -136,6 +144,19 @@ func (h *ColorizedHandler) Handle(e *lx.Entry) error { } } +// Timestamped enables or disables timestamp display and optionally sets a custom time format. +// If format is empty, defaults to RFC3339. +// Example: +// +// handler := NewColorizedHandler(os.Stdout).Timestamped(true, time.StampMilli) +// // Output: Jan 02 15:04:05.000 [app] INFO: Test +func (h *ColorizedHandler) Timestamped(enable bool, format ...string) { + h.showTime = enable + if len(format) > 0 && format[0] != "" { + h.timeFormat = format[0] + } +} + // handleRegularOutput handles normal log entries. // It formats the entry with colored namespace, level, message, fields, and stack trace (if present), // writing the result to the handler's writer. @@ -146,6 +167,12 @@ func (h *ColorizedHandler) Handle(e *lx.Entry) error { func (h *ColorizedHandler) handleRegularOutput(e *lx.Entry) error { var builder strings.Builder // Buffer for building formatted output + // Add timestamp if enabled + if h.showTime { + builder.WriteString(e.Timestamp.Format(h.timeFormat)) + builder.WriteString(lx.Space) + } + // Format namespace with colors h.formatNamespace(&builder, e) @@ -345,6 +372,13 @@ func (h *ColorizedHandler) formatStack(b *strings.Builder, stack []byte) { // h.handleDumpOutput(&lx.Entry{Class: lx.ClassDump, Message: "pos 00 hex: 61 62 'ab'"}) // Writes colored dump func (h *ColorizedHandler) handleDumpOutput(e *lx.Entry) error { var builder strings.Builder + + // Add timestamp if enabled + if h.showTime { + builder.WriteString(e.Timestamp.Format(h.timeFormat)) + builder.WriteString(lx.Newline) + } + // Write colored BEGIN separator builder.WriteString(h.palette.Title) builder.WriteString("---- BEGIN DUMP ----") diff --git a/vendor/github.com/olekukonko/ll/lh/memory.go b/vendor/github.com/olekukonko/ll/lh/memory.go index 78ac918a..e3bc9398 100644 --- a/vendor/github.com/olekukonko/ll/lh/memory.go +++ b/vendor/github.com/olekukonko/ll/lh/memory.go @@ -11,8 +11,10 @@ import ( // Useful for testing or buffering logs for later inspection. // It maintains a thread-safe slice of log entries, protected by a read-write mutex. type MemoryHandler struct { - mu sync.RWMutex // Protects concurrent access to entries - entries []*lx.Entry // Slice of stored log entries + mu sync.RWMutex // Protects concurrent access to entries + entries []*lx.Entry // Slice of stored log entries + showTime bool // Whether to show timestamps when dumping + timeFormat string // Time format for dumping } // NewMemoryHandler creates a new MemoryHandler. @@ -28,6 +30,23 @@ func NewMemoryHandler() *MemoryHandler { } } +// Timestamped enables/disables timestamp display when dumping and optionally sets a time format. +// Consistent with TextHandler and ColorizedHandler signature. +// Example: +// +// handler.Timestamped(true) // Enable with default format +// handler.Timestamped(true, time.StampMilli) // Enable with custom format +// handler.Timestamped(false) // Disable +func (h *MemoryHandler) Timestamped(enable bool, format ...string) { + h.mu.Lock() + defer h.mu.Unlock() + + h.showTime = enable + if len(format) > 0 && format[0] != "" { + h.timeFormat = format[0] + } +} + // Handle stores the log entry in memory. // It appends the provided entry to the entries slice, ensuring thread-safety with a write lock. // Always returns nil, as it does not perform I/O operations. @@ -82,6 +101,7 @@ func (h *MemoryHandler) Dump(w io.Writer) error { // Create a temporary TextHandler to format entries tempHandler := NewTextHandler(w) + tempHandler.Timestamped(h.showTime, h.timeFormat) // Process each entry through the TextHandler for _, entry := range h.entries { diff --git a/vendor/github.com/olekukonko/ll/lh/text.go b/vendor/github.com/olekukonko/ll/lh/text.go index 3b0a85ce..7423e210 100644 --- a/vendor/github.com/olekukonko/ll/lh/text.go +++ b/vendor/github.com/olekukonko/ll/lh/text.go @@ -6,6 +6,7 @@ import ( "io" "sort" "strings" + "time" ) // TextHandler is a handler that outputs log entries as plain text. @@ -13,7 +14,9 @@ import ( // writing the result to the provided writer. // Thread-safe if the underlying writer is thread-safe. type TextHandler struct { - w io.Writer // Destination for formatted log output + w io.Writer // Destination for formatted log output + showTime bool // Whether to display timestamps + timeFormat string // Format for timestamps (defaults to time.RFC3339) } // NewTextHandler creates a new TextHandler writing to the specified writer. @@ -24,7 +27,24 @@ type TextHandler struct { // logger := ll.New("app").Enable().Handler(handler) // logger.Info("Test") // Output: [app] INFO: Test func NewTextHandler(w io.Writer) *TextHandler { - return &TextHandler{w: w} + return &TextHandler{ + w: w, + showTime: false, + timeFormat: time.RFC3339, + } +} + +// Timestamped enables or disables timestamp display and optionally sets a custom time format. +// If format is empty, defaults to RFC3339. +// Example: +// +// handler := NewTextHandler(os.Stdout).TextWithTime(true, time.StampMilli) +// // Output: Jan 02 15:04:05.000 [app] INFO: Test +func (h *TextHandler) Timestamped(enable bool, format ...string) { + h.showTime = enable + if len(format) > 0 && format[0] != "" { + h.timeFormat = format[0] + } } // Handle processes a log entry and writes it as plain text. @@ -60,6 +80,12 @@ func (h *TextHandler) Handle(e *lx.Entry) error { func (h *TextHandler) handleRegularOutput(e *lx.Entry) error { var builder strings.Builder // Buffer for building formatted output + // Add timestamp if enabled + if h.showTime { + builder.WriteString(e.Timestamp.Format(h.timeFormat)) + builder.WriteString(lx.Space) + } + // Format namespace based on style switch e.Style { case lx.NestedPath: @@ -140,6 +166,12 @@ func (h *TextHandler) handleDumpOutput(e *lx.Entry) error { // For text handler, we just add a newline before dump output var builder strings.Builder // Buffer for building formatted output + // Add timestamp if enabled + if h.showTime { + builder.WriteString(e.Timestamp.Format(h.timeFormat)) + builder.WriteString(lx.Newline) + } + // Add separator lines and dump content builder.WriteString("---- BEGIN DUMP ----\n") builder.WriteString(e.Message) diff --git a/vendor/github.com/olekukonko/ll/ll.go b/vendor/github.com/olekukonko/ll/ll.go index 3a660b95..44e51512 100644 --- a/vendor/github.com/olekukonko/ll/ll.go +++ b/vendor/github.com/olekukonko/ll/ll.go @@ -1116,6 +1116,24 @@ func (l *Logger) Style(style lx.StyleType) *Logger { return l } +// Timestamped enables or disables timestamp logging for the logger and optionally sets the timestamp format. +// It is thread-safe, using a write lock to ensure safe concurrent access. +// If the logger's handler supports the lx.Timestamper interface, the timestamp settings are applied. +// The method returns the logger instance to support method chaining. +// Parameters: +// +// enable: Boolean to enable or disable timestamp logging +// format: Optional string(s) to specify the timestamp format +func (l *Logger) Timestamped(enable bool, format ...string) *Logger { + l.mu.Lock() + defer l.mu.Unlock() + + if h, ok := l.handler.(lx.Timestamper); ok { + h.Timestamped(enable, format...) + } + return l +} + // Use adds a middleware function to process log entries before they are handled, returning // a Middleware handle for removal. Middleware returning a non-nil error stops the log. // It is thread-safe using a write lock. @@ -1389,6 +1407,24 @@ func WithHandler(handler lx.Handler) Option { } } +// WithTimestamped returns an Option that configures timestamp settings for the logger's existing handler. +// It enables or disables timestamp logging and optionally sets the timestamp format if the handler +// supports the lx.Timestamper interface. If no handler is set, the function has no effect. +// Parameters: +// +// enable: Boolean to enable or disable timestamp logging +// format: Optional string(s) to specify the timestamp format +func WithTimestamped(enable bool, format ...string) Option { + return func(l *Logger) { + if l.handler != nil { // Check if a handler is set + // Verify if the handler supports the lx.Timestamper interface + if h, ok := l.handler.(lx.Timestamper); ok { + h.Timestamped(enable, format...) // Apply timestamp settings to the handler + } + } + } +} + // WithLevel sets the minimum log level for the logger as a functional option for // configuring a new logger instance. // Example: diff --git a/vendor/github.com/olekukonko/ll/lx/lx.go b/vendor/github.com/olekukonko/ll/lx/lx.go index 2e404d59..64661886 100644 --- a/vendor/github.com/olekukonko/ll/lx/lx.go +++ b/vendor/github.com/olekukonko/ll/lx/lx.go @@ -124,6 +124,16 @@ type Handler interface { Handle(e *Entry) error // Processes a log entry, returning any error } +// Timestamper defines an interface for handlers that support timestamp configuration. +// It includes a method to enable or disable timestamp logging and optionally set the timestamp format. +type Timestamper interface { + // Timestamped enables or disables timestamp logging and allows specifying an optional format. + // Parameters: + // enable: Boolean to enable or disable timestamp logging + // format: Optional string(s) to specify the timestamp format + Timestamped(enable bool, format ...string) +} + // ClassType represents the type of a log entry. // It is an integer type used to categorize log entries (Text, JSON, Dump, Special, Raw), // influencing how handlers process and format them. diff --git a/vendor/github.com/olekukonko/tablewriter/README.md b/vendor/github.com/olekukonko/tablewriter/README.md index 70480d69..2ce07851 100644 --- a/vendor/github.com/olekukonko/tablewriter/README.md +++ b/vendor/github.com/olekukonko/tablewriter/README.md @@ -28,7 +28,7 @@ go get github.com/olekukonko/tablewriter@v0.0.5 #### Latest Version The latest stable version ```bash -go get github.com/olekukonko/tablewriter@v1.0.7 +go get github.com/olekukonko/tablewriter@v1.0.9 ``` **Warning:** Version `v1.0.0` contains missing functionality and should not be used. @@ -62,7 +62,7 @@ func main() { data := [][]string{ {"Package", "Version", "Status"}, {"tablewriter", "v0.0.5", "legacy"}, - {"tablewriter", "v1.0.7", "latest"}, + {"tablewriter", "v1.0.9", "latest"}, } table := tablewriter.NewWriter(os.Stdout) @@ -77,7 +77,7 @@ func main() { │ PACKAGE │ VERSION │ STATUS │ ├─────────────┼─────────┼────────┤ │ tablewriter │ v0.0.5 │ legacy │ -│ tablewriter │ v1.0.7 │ latest │ +│ tablewriter │ v1.0.9 │ latest │ └─────────────┴─────────┴────────┘ ``` diff --git a/vendor/github.com/olekukonko/tablewriter/config.go b/vendor/github.com/olekukonko/tablewriter/config.go index 94094f1b..93f7fa34 100644 --- a/vendor/github.com/olekukonko/tablewriter/config.go +++ b/vendor/github.com/olekukonko/tablewriter/config.go @@ -688,6 +688,12 @@ func (bb *BehaviorConfigBuilder) WithCompactMerge(state tw.State) *BehaviorConfi return bb } +// WithAutoHeader enables/disables automatic header extraction for structs in Bulk. +func (bb *BehaviorConfigBuilder) WithAutoHeader(state tw.State) *BehaviorConfigBuilder { + bb.config.Structs.AutoHeader = state + return bb +} + // ColumnConfigBuilder configures column-specific settings type ColumnConfigBuilder struct { parent *ConfigBuilder diff --git a/vendor/github.com/olekukonko/tablewriter/option.go b/vendor/github.com/olekukonko/tablewriter/option.go index 7270b768..f1ea70b1 100644 --- a/vendor/github.com/olekukonko/tablewriter/option.go +++ b/vendor/github.com/olekukonko/tablewriter/option.go @@ -717,6 +717,10 @@ func defaultConfig() Config { Behavior: tw.Behavior{ AutoHide: tw.Off, TrimSpace: tw.On, + Structs: tw.Struct{ + AutoHeader: tw.Off, + Tags: []string{"json", "db"}, + }, }, } } @@ -844,6 +848,14 @@ func mergeConfig(dst, src Config) Config { dst.Behavior.Compact = src.Behavior.Compact dst.Behavior.Header = src.Behavior.Header dst.Behavior.Footer = src.Behavior.Footer + dst.Behavior.Footer = src.Behavior.Footer + + dst.Behavior.Structs.AutoHeader = src.Behavior.Structs.AutoHeader + + // check lent of tags + if len(src.Behavior.Structs.Tags) > 0 { + dst.Behavior.Structs.Tags = src.Behavior.Structs.Tags + } if src.Widths.Global != 0 { dst.Widths.Global = src.Widths.Global diff --git a/vendor/github.com/olekukonko/tablewriter/renderer/blueprint.go b/vendor/github.com/olekukonko/tablewriter/renderer/blueprint.go index 42966ebc..8cfb2a19 100644 --- a/vendor/github.com/olekukonko/tablewriter/renderer/blueprint.go +++ b/vendor/github.com/olekukonko/tablewriter/renderer/blueprint.go @@ -523,9 +523,22 @@ func (f *Blueprint) renderLine(ctx tw.Formatting) { isTotalPattern := false + // Case-insensitive check for "total" + if isHMergeStart && colIndex > 0 { + if prevCellCtx, ok := ctx.Row.Current[colIndex-1]; ok { + if strings.Contains(strings.ToLower(prevCellCtx.Data), "total") { + isTotalPattern = true + f.logger.Debugf("renderLine: total pattern in row in %d", colIndex) + } + } + } + + // Get the alignment from the configuration + align = cellCtx.Align + // Override alignment for footer merged cells if (ctx.Row.Position == tw.Footer && isHMergeStart) || isTotalPattern { - if align != tw.AlignRight { + if align == tw.AlignNone { f.logger.Debugf("renderLine: Applying AlignRight HMerge/TOTAL override for Footer col %d. Original/default align was: %s", colIndex, align) align = tw.AlignRight } diff --git a/vendor/github.com/olekukonko/tablewriter/stream.go b/vendor/github.com/olekukonko/tablewriter/stream.go index 7467e9cb..d1c6e99f 100644 --- a/vendor/github.com/olekukonko/tablewriter/stream.go +++ b/vendor/github.com/olekukonko/tablewriter/stream.go @@ -1,7 +1,6 @@ package tablewriter import ( - "fmt" "github.com/olekukonko/errors" "github.com/olekukonko/tablewriter/pkg/twwidth" "github.com/olekukonko/tablewriter/tw" @@ -90,7 +89,7 @@ func (t *Table) Start() error { if !t.renderer.Config().Streaming { // Check if the configured renderer actually supports streaming. t.logger.Error("Configured renderer does not support streaming.") - return fmt.Errorf("renderer does not support streaming") + return errors.Newf("renderer does not support streaming") } //t.renderer.Start(t.writer) @@ -208,7 +207,7 @@ func (t *Table) streamAppendRow(row interface{}) error { rawCellsSlice, err := t.convertCellsToStrings(row, t.config.Row) if err != nil { t.logger.Errorf("streamAppendRow: Failed to convert row to strings: %v", err) - return fmt.Errorf("failed to convert row to strings: %w", err) + return errors.Newf("failed to convert row to strings").Wrap(err) } if len(rawCellsSlice) == 0 { @@ -221,7 +220,7 @@ func (t *Table) streamAppendRow(row interface{}) error { } if err := t.ensureStreamWidthsCalculated(rawCellsSlice, t.config.Row); err != nil { - return fmt.Errorf("failed to establish stream column count/widths: %w", err) + return errors.New("failed to establish stream column count/widths").Wrap(err) } // Now, check for column mismatch if a column count has been established. diff --git a/vendor/github.com/olekukonko/tablewriter/tablewriter.go b/vendor/github.com/olekukonko/tablewriter/tablewriter.go index 4aed1a64..e96cc93c 100644 --- a/vendor/github.com/olekukonko/tablewriter/tablewriter.go +++ b/vendor/github.com/olekukonko/tablewriter/tablewriter.go @@ -2,7 +2,6 @@ package tablewriter import ( "bytes" - "fmt" "github.com/olekukonko/errors" "github.com/olekukonko/ll" "github.com/olekukonko/ll/lh" @@ -180,65 +179,87 @@ func (t *Table) Caption(caption tw.Caption) *Table { // This is the one we modif // This method always contributes to a single logical row in the table. // To add multiple distinct rows, call Append multiple times (once for each row's data) // or use the Bulk() method if providing a slice where each element is a row. -func (t *Table) Append(rows ...interface{}) error { // rows is already []interface{} +func (t *Table) Append(rows ...interface{}) error { t.ensureInitialized() if t.config.Stream.Enable && t.hasPrinted { + // Streaming logic remains unchanged, as AutoHeader is a batch-mode concept. t.logger.Debugf("Append() called in streaming mode with %d items for a single row", len(rows)) var rowItemForStream interface{} if len(rows) == 1 { rowItemForStream = rows[0] } else { - rowItemForStream = rows // Pass the slice of items if multiple args + rowItemForStream = rows } if err := t.streamAppendRow(rowItemForStream); err != nil { t.logger.Errorf("Error rendering streaming row: %v", err) - return fmt.Errorf("failed to stream append row: %w", err) + return errors.Newf("failed to stream append row").Wrap(err) } return nil } - //Batch Mode Logic + // Batch Mode Logic t.logger.Debugf("Append (Batch) received %d arguments: %v", len(rows), rows) var cellsSource interface{} if len(rows) == 1 { cellsSource = rows[0] - t.logger.Debug("Append (Batch): Single argument provided. Treating it as the source for row cells.") } else { - cellsSource = rows // 'rows' is []interface{} containing all arguments - t.logger.Debug("Append (Batch): Multiple arguments provided. Treating them directly as cells for one row.") + cellsSource = rows + } + // Check if we should attempt to auto-generate headers from this append operation. + // Conditions: AutoHeader is on, no headers are set yet, and this is the first data row. + isFirstRow := len(t.rows) == 0 + if t.config.Behavior.Structs.AutoHeader.Enabled() && len(t.headers) == 0 && isFirstRow { + t.logger.Debug("Append: Triggering AutoHeader for the first row.") + headers := t.extractHeadersFromStruct(cellsSource) + if len(headers) > 0 { + // Set the extracted headers. The Header() method handles the rest. + t.Header(headers) + } } - if err := t.appendSingle(cellsSource); err != nil { + // The rest of the function proceeds as before, converting the data to string lines. + lines, err := t.toStringLines(cellsSource, t.config.Row) + if err != nil { t.logger.Errorf("Append (Batch) failed for cellsSource %v: %v", cellsSource, err) return err } + t.rows = append(t.rows, lines) t.logger.Debugf("Append (Batch) completed for one row, total rows in table: %d", len(t.rows)) return nil } -// Bulk adds multiple rows from a slice to the table (legacy method). -// Parameter rows must be a slice compatible with stringer or []string. -// Returns an error if the input is invalid or appending fails. +// Bulk adds multiple rows from a slice to the table. +// If Behavior.AutoHeader is enabled, no headers set, and rows is a slice of structs, +// automatically extracts/sets headers from the first struct. func (t *Table) Bulk(rows interface{}) error { - t.logger.Debug("Starting Bulk operation") rv := reflect.ValueOf(rows) if rv.Kind() != reflect.Slice { - err := errors.Newf("Bulk expects a slice, got %T", rows) - t.logger.Debugf("Bulk error: %v", err) - return err + return errors.Newf("Bulk expects a slice, got %T", rows) + } + if rv.Len() == 0 { + return nil } + + // AutoHeader logic remains here, as it's a "Bulk" operation concept. + if t.config.Behavior.Structs.AutoHeader.Enabled() && len(t.headers) == 0 { + first := rv.Index(0).Interface() + // We can now correctly get headers from pointers or embedded structs + headers := t.extractHeadersFromStruct(first) + if len(headers) > 0 { + t.Header(headers) + } + } + + // The rest of the logic is now just a loop over Append. for i := 0; i < rv.Len(); i++ { row := rv.Index(i).Interface() - t.logger.Debugf("Processing bulk row %d: %v", i, row) - if err := t.appendSingle(row); err != nil { - t.logger.Debugf("Bulk append failed at index %d: %v", i, err) + if err := t.Append(row); err != nil { // Use Append return err } } - t.logger.Debugf("Bulk completed, processed %d rows", rv.Len()) return nil } @@ -1383,13 +1404,13 @@ func (t *Table) render() error { if err != nil { t.writer = originalWriter t.logger.Errorf("prepareContexts failed: %v", err) - return fmt.Errorf("failed to prepare table contexts: %w", err) + return errors.Newf("failed to prepare table contexts").Wrap(err) } if err := ctx.renderer.Start(t.writer); err != nil { t.writer = originalWriter t.logger.Errorf("Renderer Start() error: %v", err) - return fmt.Errorf("renderer start failed: %w", err) + return errors.Newf("renderer start failed").Wrap(err) } renderError := false @@ -1404,7 +1425,7 @@ func (t *Table) render() error { if renderErr := renderFn(ctx, mctx); renderErr != nil { t.logger.Errorf("Renderer section error (%s): %v", sectionName, renderErr) if !renderError { - firstRenderErr = fmt.Errorf("failed to render %s section: %w", sectionName, renderErr) + firstRenderErr = errors.Newf("failed to render %s section", sectionName).Wrap(renderErr) } renderError = true break @@ -1414,7 +1435,7 @@ func (t *Table) render() error { if closeErr := ctx.renderer.Close(); closeErr != nil { t.logger.Errorf("Renderer Close() error: %v", closeErr) if !renderError { - firstRenderErr = fmt.Errorf("renderer close failed: %w", closeErr) + firstRenderErr = errors.Newf("renderer close failed").Wrap(closeErr) } renderError = true } diff --git a/vendor/github.com/olekukonko/tablewriter/tw/types.go b/vendor/github.com/olekukonko/tablewriter/tw/types.go index 29a1862c..c3cef667 100644 --- a/vendor/github.com/olekukonko/tablewriter/tw/types.go +++ b/vendor/github.com/olekukonko/tablewriter/tw/types.go @@ -141,6 +141,18 @@ type Compact struct { Merge State // Merge enables compact width calculation during cell merging, optimizing space allocation. } +// Struct holds settings for struct-based operations like AutoHeader. +type Struct struct { + // AutoHeader automatically extracts and sets headers from struct fields when Bulk is called with a slice of structs. + // Uses JSON tags if present, falls back to field names (title-cased). Skips unexported or json:"-" fields. + // Enabled by default for convenience. + AutoHeader State + + // Tags is a priority-ordered list of struct tag keys to check for header names. + // The first tag found on a field will be used. Defaults to ["json", "db"]. + Tags []string +} + // Behavior defines settings that control table rendering behaviors, such as column visibility and content formatting. type Behavior struct { AutoHide State // AutoHide determines whether empty columns are hidden. Ignored in streaming mode. @@ -152,6 +164,9 @@ type Behavior struct { // Compact enables optimized width calculation for merged cells, such as in horizontal merges, // by systematically determining the most efficient width instead of scaling by the number of columns. Compact Compact + + // Structs contains settings for how struct data is processed. + Structs Struct } // Padding defines the spacing characters around cell content in all four directions. diff --git a/vendor/github.com/olekukonko/tablewriter/zoo.go b/vendor/github.com/olekukonko/tablewriter/zoo.go index b24f230c..e0fee2cf 100644 --- a/vendor/github.com/olekukonko/tablewriter/zoo.go +++ b/vendor/github.com/olekukonko/tablewriter/zoo.go @@ -1197,6 +1197,10 @@ func (t *Table) convertToString(value interface{}) string { // convertItemToCells is responsible for converting a single input item (which could be // a struct, a basic type, or an item implementing Stringer/Formatter) into a slice // of strings, where each string represents a cell for the table row. +// zoo.go + +// convertItemToCells is responsible for converting a single input item into a slice of strings. +// It now uses the unified struct parser for structs. func (t *Table) convertItemToCells(item interface{}) ([]string, error) { t.logger.Debugf("convertItemToCells: Converting item of type %T", item) @@ -1204,10 +1208,10 @@ func (t *Table) convertItemToCells(item interface{}) ([]string, error) { if t.stringer != nil { res, err := t.convertToStringer(item) if err == nil { - t.logger.Debugf("convertItemToCells: Used custom table stringer (t.stringer) for type %T. Produced %d cells: %v", item, len(res), res) + t.logger.Debugf("convertItemToCells: Used custom table stringer for type %T. Produced %d cells: %v", item, len(res), res) return res, nil } - t.logger.Warnf("convertItemToCells: Custom table stringer (t.stringer) was set but incompatible or errored for type %T: %v. Will attempt other conversion methods.", item, err) + t.logger.Warnf("convertItemToCells: Custom table stringer was set but incompatible for type %T: %v. Will attempt other methods.", item, err) } // 2. Handle untyped nil directly. @@ -1216,85 +1220,26 @@ func (t *Table) convertItemToCells(item interface{}) ([]string, error) { return []string{""}, nil } - itemValue := reflect.ValueOf(item) - itemType := itemValue.Type() - - // 3. Handle pointers: Dereference pointers to get to the underlying struct or value. - if itemType.Kind() == reflect.Ptr { - if itemValue.IsNil() { - t.logger.Debugf("convertItemToCells: Item is a nil pointer of type %s. Returning single empty cell.", itemType.String()) - return []string{""}, nil - } - itemValue = itemValue.Elem() - itemType = itemValue.Type() - t.logger.Debugf("convertItemToCells: Dereferenced pointer, now processing type %s.", itemType.String()) + // 3. Use the new unified struct parser. It handles pointers and embedding. + // We only care about the values it returns. + _, values := t.extractFieldsAndValuesFromStruct(item) + if values != nil { + t.logger.Debugf("convertItemToCells: Structs %T reflected into %d cells: %v", item, len(values), values) + return values, nil } - // 4. Special handling for structs: - if itemType.Kind() == reflect.Struct { - // Check if the original item (before potential dereference) implements Formatter or Stringer. - if formatter, ok := item.(tw.Formatter); ok { - t.logger.Debugf("convertItemToCells: Struct item (type %s) is tw.Formatter. Using Format(). Resulting in 1 cell.", itemType.Name()) - return []string{formatter.Format()}, nil - } - if stringer, ok := item.(fmt.Stringer); ok { - t.logger.Debugf("convertItemToCells: Struct item (type %s) is fmt.Stringer. Using String(). Resulting in 1 cell.", itemType.Name()) - return []string{stringer.String()}, nil - } - - t.logger.Debugf("convertItemToCells: Item is a struct (type %s). Attempting generic field reflection to expand into multiple cells.", itemType.Name()) - numFields := itemValue.NumField() - structCells := make([]string, 0, numFields) - hasProcessableFields := false - - for i := 0; i < numFields; i++ { - fieldMeta := itemType.Field(i) - if fieldMeta.PkgPath != "" { - t.logger.Debugf("convertItemToCells: Skipping unexported field %s in struct %s", fieldMeta.Name, itemType.Name()) - continue - } - hasProcessableFields = true // Mark true if we encounter any exported field - - jsonTag := fieldMeta.Tag.Get("json") - if jsonTag == "-" { - t.logger.Debugf("convertItemToCells: Skipping field %s in struct %s due to json:\"-\" tag", fieldMeta.Name, itemType.Name()) - continue - } - - fieldReflectedValue := itemValue.Field(i) - if strings.Contains(jsonTag, ",omitempty") && fieldReflectedValue.IsZero() { - t.logger.Debugf("convertItemToCells: Omitting zero value for field %s in struct %s due to omitempty tag", fieldMeta.Name, itemType.Name()) - structCells = append(structCells, "") - continue - } - structCells = append(structCells, t.convertToString(fieldReflectedValue.Interface())) - } - - // Only return expanded cells if there were processable fields. - // If a struct has no exported fields, or all were skipped via json:"-", - // it should still produce output (e.g. fmt.Sprintf of the struct) rather than an empty row. - if hasProcessableFields { - t.logger.Debugf("convertItemToCells: Struct %s reflected into %d cells: %v", itemType.Name(), len(structCells), structCells) - return structCells, nil - } - - t.logger.Warnf("convertItemToCells: Struct %s has no processable exported fields. Falling back to Sprintf for the whole item (resulting in 1 cell).", itemType.Name()) - return []string{t.convertToString(item)}, nil // 'item' is the original potentially pointer type - } - - // 5. Item is NOT a struct. It might be a basic type or a non-struct type implementing Formatter/Stringer. - // These should all result in a single cell. + // 4. Fallback for any other single item (e.g., basic types, or types that implement Stringer/Formatter). + // This code path is now for non-struct types. if formatter, ok := item.(tw.Formatter); ok { - t.logger.Debugf("convertItemToCells: Item (non-struct, type %T) is tw.Formatter. Using Format(). Resulting in 1 cell.", item) + t.logger.Debugf("convertItemToCells: Item (non-struct, type %T) is tw.Formatter. Using Format().", item) return []string{formatter.Format()}, nil } if stringer, ok := item.(fmt.Stringer); ok { - t.logger.Debugf("convertItemToCells: Item (non-struct, type %T) is fmt.Stringer. Using String(). Resulting in 1 cell.", item) + t.logger.Debugf("convertItemToCells: Item (non-struct, type %T) is fmt.Stringer. Using String().", item) return []string{stringer.String()}, nil } - // 6. Fallback for any other single item (e.g., basic types like int, string, bool): - t.logger.Debugf("convertItemToCells: Item (type %T) is a basic type or unhandled by other mechanisms. Treating as single cell via convertToString.", item) + t.logger.Debugf("convertItemToCells: Item (type %T) is a basic type. Treating as single cell via convertToString.", item) return []string{t.convertToString(item)}, nil } @@ -1694,3 +1639,92 @@ func (t *Table) updateWidths(row []string, widths tw.Mapper[int, int], padding t } } } + +// extractHeadersFromStruct is now a thin wrapper around the new unified function. +// It only cares about the header names. +func (t *Table) extractHeadersFromStruct(sample interface{}) []string { + headers, _ := t.extractFieldsAndValuesFromStruct(sample) + return headers +} + +// extractFieldsAndValuesFromStruct is the new single source of truth for struct reflection. +// It recursively processes a struct, handling pointers and embedded structs, +// and returns two slices: one for header names and one for string-converted values. +func (t *Table) extractFieldsAndValuesFromStruct(sample interface{}) ([]string, []string) { + v := reflect.ValueOf(sample) + if v.Kind() == reflect.Ptr { + if v.IsNil() { + return nil, nil + } + v = v.Elem() + } + + if v.Kind() != reflect.Struct { + return nil, nil + } + + typ := v.Type() + headers := make([]string, 0, typ.NumField()) + values := make([]string, 0, typ.NumField()) + + for i := 0; i < typ.NumField(); i++ { + field := typ.Field(i) + fieldValue := v.Field(i) + + // Skip unexported fields + if field.PkgPath != "" { + continue + } + + // Handle embedded structs recursively + if field.Anonymous { + h, val := t.extractFieldsAndValuesFromStruct(fieldValue.Interface()) + if h != nil { + headers = append(headers, h...) + values = append(values, val...) + } + continue + } + + var tagName string + skipField := false + + // Loop through the priority list of configured tags (e.g., ["json", "db"]) + for _, tagKey := range t.config.Behavior.Structs.Tags { + tagValue := field.Tag.Get(tagKey) + + // If a tag is found... + if tagValue != "" { + // If the tag is "-", this field should be skipped entirely. + if tagValue == "-" { + skipField = true + break // Stop processing tags for this field. + } + // Otherwise, we've found our highest-priority tag. Store it and stop. + tagName = tagValue + break // Stop processing tags for this field. + } + } + + // If the field was marked for skipping, continue to the next field. + if skipField { + continue + } + + // Determine header name from the tag or fallback to the field name + headerName := field.Name + if tagName != "" { + headerName = strings.Split(tagName, ",")[0] + } + headers = append(headers, tw.Title(headerName)) + + // Determine value, respecting omitempty from the found tag + value := "" + if !strings.Contains(tagName, ",omitempty") || !fieldValue.IsZero() { + value = t.convertToString(fieldValue.Interface()) + } + values = append(values, value) + } + + return headers, values +} diff --git a/vendor/modules.txt b/vendor/modules.txt index c19247ed..609c8cdd 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -175,15 +175,15 @@ github.com/mattn/go-runewidth # github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 ## explicit github.com/monochromegane/go-gitignore -# github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6 +# github.com/olekukonko/errors v1.1.0 ## explicit; go 1.21 github.com/olekukonko/errors -# github.com/olekukonko/ll v0.0.8 +# github.com/olekukonko/ll v0.0.9 ## explicit; go 1.21 github.com/olekukonko/ll github.com/olekukonko/ll/lh github.com/olekukonko/ll/lx -# github.com/olekukonko/tablewriter v1.0.8 +# github.com/olekukonko/tablewriter v1.0.9 ## explicit; go 1.21 github.com/olekukonko/tablewriter github.com/olekukonko/tablewriter/pkg/twwarp