You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
229 lines
8.7 KiB
229 lines
8.7 KiB
// =============================================================================
|
|
// Active-GO Framework
|
|
// Copyright (c) 2025 ActiveThing (https://activething.com)
|
|
// Author: Juan V. Navarro juanvnl@activething.com
|
|
// =============================================================================
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
// of this software and associated documentation files (the "Software"), to deal
|
|
// in the Software without restriction, including without limitation the rights
|
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
// copies of the Software, and to permit persons to whom the Software is
|
|
// furnished to do so, subject to the following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be included in
|
|
// all copies or substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
// THE SOFTWARE.
|
|
//
|
|
// =============================================================================
|
|
// Date Create 07/05/2025
|
|
// =============================================================================
|
|
|
|
// Package errs provides a robust infrastructure for structured error management
|
|
// in Go, specifically designed for distributed systems and microservices.
|
|
// It allows for hierarchical error classification, contextual metadata composition,
|
|
// and rich formatting to facilitate debugging and monitoring.
|
|
package errs
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
// errModuleSep is the separator used between the module name and the error class.
|
|
errModuleSep string = "::"
|
|
// errClassSep is the separator used between subclasses of an error.
|
|
errClassSep string = "."
|
|
|
|
// moduleName defines the default module name for internal package errors.
|
|
moduleName = errModuleSep
|
|
)
|
|
|
|
const (
|
|
// MetaClassName is the standard key name for the metadata representing the error class.
|
|
MetaClassName string = "class"
|
|
// MetaCauseName is the standard key name for the metadata representing the cause of an error.
|
|
MetaCauseName string = "cause"
|
|
// MetaStackName is the standard key name for the metadata representing the cause of an error.
|
|
MetaStackName string = "stack"
|
|
)
|
|
|
|
var (
|
|
// g contains global variables and configurations for the errs package.
|
|
g = struct {
|
|
// Regular expression validators for metadata names, subclasses, and error classes.
|
|
metaValidator,
|
|
subclassValidator,
|
|
classValidator *regexp.Regexp
|
|
|
|
// formatOptions defines the formatting options for error representation.
|
|
formatOptions *FormatOptions
|
|
// errorFormatter is the function used to format errors.
|
|
errorFormatter func(w io.Writer, e error, o *FormatOptions, l int, s FormatShow)
|
|
}{
|
|
// metaValidator checks if a metadata name is valid.
|
|
metaValidator: regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9_-]{0,62}[a-zA-Z0-9]$`),
|
|
// classValidator checks if an error class name (including optional module) is valid.
|
|
classValidator: regexp.MustCompile(`^(?:[a-zA-Z0-9](?:[a-zA-Z0-9._-]*[a-zA-Z0-9])?|)::[a-zA-Z0-9](?:[a-zA-Z0-9._-]*[a-zA-Z0-9])?$`),
|
|
// subclassValidator checks if a subclass name is valid.
|
|
subclassValidator: regexp.MustCompile("^[a-zA-Z0-9](?:[a-zA-Z0-9._-]*[a-zA-Z0-9])?$"),
|
|
|
|
// formatOptions is initialized with default color formatting options.
|
|
formatOptions: NewFormatOptionsColor(),
|
|
// errorFormatter is initialized with the default error formatting function.
|
|
errorFormatter: defaultFormatError,
|
|
}
|
|
|
|
// E contains a collection of predefined errors used internally by the errs package.
|
|
E = struct {
|
|
// Multiple indicates that an operation resulted in multiple errors.
|
|
Multiple,
|
|
// InvalidNilError indicates that a nil error was provided where not allowed.
|
|
InvalidNilError,
|
|
// InvalidClass indicates that the provided error class name is invalid.
|
|
InvalidClass,
|
|
// InvalidErrValue indicates that the provided Err value is invalid.
|
|
InvalidErrValue,
|
|
// InvalidMetaName indicates that the provided metadata name is invalid.
|
|
InvalidMetaName,
|
|
// InvalidMetaType indicates that the provided metadata type is invalid.
|
|
InvalidMetaType,
|
|
// InvalidNilCause indicates that a nil cause was provided where not allowed.
|
|
InvalidNilCause Err
|
|
}{
|
|
Multiple: Err(moduleName + "multiple_errors"),
|
|
InvalidErrValue: Err(moduleName + "invalid_err_name"),
|
|
InvalidClass: Err(moduleName + "invalid_class"),
|
|
InvalidNilError: Err(moduleName + "invalid_nil_error"),
|
|
InvalidMetaName: Err(moduleName + "invalid_meta_name"),
|
|
InvalidMetaType: Err(moduleName + "invalid_meta_type"),
|
|
InvalidNilCause: Err(moduleName + "invalid_nil_cause"),
|
|
}
|
|
)
|
|
|
|
// CreateSubclassOf creates a new subclass of an existing parent error.
|
|
// The subclass name is appended to the parent error name using errClassSep.
|
|
// Panic if the parent or subclass are not valid error names.
|
|
func CreateSubclassOf(parent Err, subclass string) Err {
|
|
if !IsValidSubclass(parent, subclass) {
|
|
panic(CreateParamErr("subclass", subclass))
|
|
}
|
|
return Err(string(parent) + errClassSep + subclass)
|
|
}
|
|
|
|
// ToError converts a value of any type to an error.
|
|
// If the value is nil, it returns nil.
|
|
// If the value is an error, it returns it directly.
|
|
// If the value is a slice of errors ([]error), it returns a composed error
|
|
// with E.Multiple and the causes attached (if the slice is not empty).
|
|
// For any other type, it uses fmt.Errorf to create a basic error.
|
|
func ToError(err any) error {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
switch e := err.(type) {
|
|
case error:
|
|
return e
|
|
case []error:
|
|
if len(e) == 0 {
|
|
return nil
|
|
}
|
|
//todo change a multiError
|
|
//return ercs.Compose(E.Multiple, ercs.WithCauses(e...))
|
|
}
|
|
return fmt.Errorf("%v", err)
|
|
}
|
|
|
|
// ErrorToString converts an error to its string representation.
|
|
// If the error is nil, it returns an empty string.
|
|
// If the error is of type Err, it returns its underlying string value.
|
|
// If the error implements fmt.Stringer, it uses its String() method.
|
|
// Otherwise, it uses the error's Error() method.
|
|
func ErrorToString(err error) string {
|
|
if err == nil {
|
|
return ""
|
|
}
|
|
|
|
switch e := err.(type) {
|
|
case Err:
|
|
return string(e)
|
|
case fmt.Stringer:
|
|
return e.String()
|
|
default:
|
|
return err.Error()
|
|
}
|
|
}
|
|
|
|
// IsSubclassOf checks if 'err' is a subclass of 'parent'.
|
|
// This is checked for both Err types (using the hierarchy defined by errClassSep)
|
|
// and standard errors (checking if the error string starts with the parent string + ".").
|
|
// Returns false if either error is nil.
|
|
func IsSubclassOf(err, parent error) bool {
|
|
if err == nil || parent == nil {
|
|
return false
|
|
}
|
|
|
|
// Specific check for Err types
|
|
var e, p Err
|
|
if errors.As(err, &e) && errors.As(parent, &p) {
|
|
return e.IsSubclassOf(p)
|
|
}
|
|
|
|
// Generic check for errors implementing Unwrap
|
|
for e := err; e != nil; e = errors.Unwrap(e) {
|
|
errStr := ErrorToString(e)
|
|
parentStr := ErrorToString(parent)
|
|
if errStr == parentStr || strings.HasPrefix(errStr, parentStr+".") {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// IsValidErr checks if a string is a valid error name according to the rules defined by classValidator.
|
|
func IsValidErr(value string) bool {
|
|
return len(value) > 0 && g.classValidator.MatchString(value)
|
|
}
|
|
|
|
// IsValidClass checks if a string is a valid class name according to the rules defined by classValidator.
|
|
func IsValidClass(class string) bool {
|
|
return len(class) > 0 && g.classValidator.MatchString(class)
|
|
}
|
|
|
|
// IsValidSubclass checks if 'subclass' is a valid subclass name for the 'parent' error.
|
|
// It verifies that both the parent and subclass are valid names and that the subclass is not identical
|
|
// to the final name of the parent (to prevent subclasses from having the same name as their direct parent).
|
|
func IsValidSubclass(parent Err, subclass string) bool {
|
|
if !IsValidErr(string(parent)) {
|
|
return false
|
|
}
|
|
if !IsValidClass(subclass) { // Should this be IsValidSubclass name?
|
|
return false
|
|
}
|
|
|
|
// Extract the final name of the parent class (e.g., "format" from "modulo::validation.format")
|
|
parentName := string(parent)
|
|
if idx := strings.LastIndex(parentName, errClassSep); idx != -1 {
|
|
parentFinalName := parentName[idx+len(errClassSep):]
|
|
return subclass != parentFinalName
|
|
}
|
|
// If no errClassSep, use the class name after errModuleSep
|
|
if c, h := strings.CutPrefix(parentName, errModuleSep); h {
|
|
return subclass != c
|
|
}
|
|
// If neither errClassSep nor errModuleSep is present, compare with the full parent name.
|
|
return subclass != parentName
|
|
}
|
|
|