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

// =============================================================================
// 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
}

Powered by TurnKey Linux.