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.

367 lines
8.8 KiB

package errs
import (
"core/btes"
"errors"
"strings"
)
const (
ModuleName = Module("errs")
ErrInvalidModule = Code(ModuleName + ModuleSep + "invalid_module_name")
ErrInvalidCode = Code(ModuleName + ModuleSep + "invalid_error_code")
ErrInvalidCodeLen = Code(string(ErrInvalidModule) + string(CodeSep) + "invalid_len")
ErrInvalidCodeChar = Code(string(ErrInvalidModule) + string(CodeSep) + "invalid_char")
)
var (
// isValidChar es el predicado base que define el conjunto de caracteres permitidos
// para los segmentos atómicos de un Code (nombres de módulo y términos de error).
//
// Al estar centralizado en esta variable, aseguramos consistencia en todo el
// paquete errs. Por defecto, utiliza btes.ASCII.IsAlphaLowerNum para imponer:
// - Minúsculas [a-z]
// - Números [0-9]
//
// Nota: Los separadores ('.', '_', ':') se gestionan en la lógica estructural
// superior (ValidateCode), no aquí
//
isValidChar func(ch byte) bool = btes.ASCII.IsAlphaLowerNum
)
const (
// Minima longitud para un Code que sería
// la longitud minima para módulo, más
// la longitud del separador de módulo,
// más la longitud minima del segmento de error
// a::b
minCodeSeg = 1
minCodeLength = minModuleSeg + lenModuleSep + minCodeSeg
wordSep = '_'
codeSepChar = '.'
CodeSep = string(codeSepChar)
lenCodeSep = 1
)
// Code representa un identificador único de error con estructura jerárquica.
// El formato sigue el patrón: "modulo::categoria.subcategoria.error"
//
// Reglas de Estructura:
// 1. Módulo: Es obligatorio y no puede contener puntos ni separadores.
// 2. Separador de Módulo: Utiliza exactamente"::" para dividir el origen del error de su contexto.
// 3. Jerarquía de Error: La parte derecha puede contener puntos para definir niveles de detalle.
//
// Reglas de Caracteres:
// - Solo se permiten minúsculas [a-z] y números [0-9].
// - El único separador de palabras permitido es el guion bajo '_'.
// - No se permiten separadores ('.', '_', ':') al inicio o al final del código.
// - No se permiten separadores consecutivos (ej. "..", "::.", "._").
type Code string
func (c Code) Sub(segment string) Code {
if !isValidSegment(segment, minCodeSeg) {
panic(ErrInvalidCode)
}
return c + Code(CodeSep+segment)
}
func (c Code) Module() (Module, error) {
s := string(c)
if i := strings.Index(s, ModuleSep); i != -1 {
return Module(s[:i]), nil
}
return "", ErrInvalidModule
}
// Parent devuelve el nivel superior de la jerarquía de error.
// Si el código no tiene jerarquía interna (solo módulo + error), devuelve: "".
//
// Ejemplos:
//
// "auth::user.login.failed" -> "auth::user.login"
// "auth::user" -> ""
func (c Code) Parent() string {
s := string(c)
if i := strings.LastIndex(s, CodeSep); i >= 0 {
return s[:i]
}
return ""
}
// Leaf devuelve únicamente el segmento final del código (el más específico).
//
// Ejemplos:
//
// "auth::user.login.failed" -> "failed"
// "auth::user" -> "user"
func (c Code) Leaf() string {
s := string(c)
if i := strings.LastIndex(s, CodeSep); i >= 0 {
return s[i+1:]
}
if i := strings.LastIndex(s, ModuleSep); i >= 0 {
return s[i+lenModuleSep:]
}
return s
}
func (c Code) Error() string {
return string(c)
}
func (c Code) String() string {
return string(c)
}
func (c Code) Is(target error) bool {
var t Code
return errors.As(target, &t) && c == t
}
// IsDescendantOf verifica si el código pertenece a la línea sucesoria del padre.
// Devuelve true si es un descendiente jerárquico.
//
// Ejemplo:
//
// "auth::login.failed".IsDescendantOf("auth::login") -> true
// "auth::login".IsDescendantOf("auth::login") -> true
func (c Code) IsDescendantOf(err error) bool {
var p Code
if !errors.As(err, &p) {
return false
}
cd, ps := string(c), string(p)
if cd == ps {
return false
}
return strings.HasPrefix(cd, ps+CodeSep)
}
// IsOrDescendantOf verifica si el código es idéntico al objetivo o si desciende de él.
// Es la herramienta principal para realizar comprobaciones jerárquicas.
//
// Ejemplos:
//
// "auth::login.failed".IsOrDescendantOf("auth::login") -> true (descendiente)
// "auth::login".IsOrDescendantOf("auth::login") -> true (identidad)
// "auth::login".IsOrDescendantOf("auth::logout") -> false
func (c Code) IsOrDescendantOf(err error) bool {
var t Code
if !errors.As(err, &t) {
return false
}
sc, st := string(c), string(t)
if sc == st {
return true
}
return strings.HasPrefix(sc, st+string(codeSepChar))
}
// Parts devuelve las partes del código: [módulo, segmento1, segmento2, ...]
// La primera celda del slice corresponde al módulo,
// las siguientes celdas a cada uno de los segmentos del código.
//
// Ejemplos:
//
// "auth::failed" → ["auth", "failed"], nil
// "auth::failed.invalid-token" → ["auth", "failed", "invalid-token"], nil
// "auth::" → nil, ErrInvalidCode (no hay código después del módulo)
// "auth" → nil, ErrInvalidCode (falta separador de módulo)
func (c Code) Parts() ([]string, error) {
s := string(c)
n := len(s)
moduleEnd := strings.Index(s, ModuleSep)
if moduleEnd == -1 {
return nil, ErrInvalidModule
}
if moduleEnd+lenModuleSep >= n {
return nil, ErrInvalidCode
}
// Extraer módulo
var segments []string
segments = append(segments, s[:moduleEnd])
start := moduleEnd + lenModuleSep
for i := start; i <= n; i++ {
if i == n || s[i] == CodeSep[0] {
segment := s[start:i]
if len(segment) > 0 {
segments = append(segments, segment)
}
start = i + 1
}
}
return segments, nil
}
// ValidateCode verifica si un string cumple las reglas de formato de Code.
//
// # Reglas de Validación
//
// 1. Longitud mínima: 4 caracteres ("m::e")
// 2. Debe contener exactamente un separador de módulo "::"
// 3. Debe empezar con alfanumérico (no '.', ':', '-', '_')
// 4. Debe terminar con alfanumérico (no '.', ':', '-', '_')
// 5. Después de separadores ('::' o '.') debe venir alfanumérico
// 6. No puede tener separadores consecutivos ('..' o '::.' o '.::')
// 7. No puede tener '.' antes del '::'
// 8. Solo permite: a-z, A-Z, 0-9, '_', '.', '::'
//
// # Ejemplos Válidos
//
// "database::connection"
// "database::connection.timeout"
// "http::request.validation.missing_field"
//
// # Ejemplos Inválidos
//
// "error" // Sin módulo (falta ::)
// "::error" // Empieza con ::
// "mod::" // Termina con ::
// "mod..err" // Separadores consecutivos
// "mod.cat::err" // '.' antes de '::'
// "auth::inicio._sesion" // '_' después de '.'
// "auth::sin_apostrofe-" // Termina con '-'
// "auth::sin_apostrofe2_" // Termina con '-'
func ValidateCode(code string) error {
ln := len(code)
if ln < minCodeLength {
return ErrInvalidCodeLen
}
if !isValidChar(code[0]) || !isValidChar(code[ln-1]) {
return ErrInvalidCodeChar
}
hasModuleSep := false
prevWasSep := false
for i := 0; i < ln; i++ {
ch := code[i]
if isValidChar(ch) {
prevWasSep = false
continue
}
if ch == wordSep {
if prevWasSep {
return ErrInvalidCodeChar
}
prevWasSep = false
continue
}
if ch == moduleSepChar {
if hasModuleSep {
return ErrInvalidCodeChar
}
if i+1 >= ln || code[i+1] != moduleSepChar {
return ErrInvalidCodeChar
}
if prevWasSep {
return ErrInvalidCodeChar
}
hasModuleSep = true
prevWasSep = true
i++
continue
}
if ch == codeSepChar {
if !hasModuleSep {
return ErrInvalidCodeChar
}
if prevWasSep {
return ErrInvalidCodeChar
}
prevWasSep = true
continue
}
return ErrInvalidCodeChar
}
if !hasModuleSep {
return ErrInvalidModule
}
return nil
}
func isValidSegment(seg string, minLen int) bool {
ln := len(seg)
if ln < minLen {
return false
}
if seg[0] == wordSep || seg[ln-1] == wordSep {
return false
}
for i := 0; i < ln; i++ {
ch := seg[i]
if isValidChar(ch) || ch == wordSep {
continue
}
return false
}
return true
}
const (
moduleSepChar = ':'
ModuleSep = "::"
lenModuleSep = 2
minModuleSeg = 1
)
type Module string
// MakeModule crea un nuevo espacio de nombres para errores.
// Solo acepta nombres "puros" (ej. "auth", no "auth::").
// Si el nombre contiene caracteres no permitidos o el separador '::', lanza un pánico.
func MakeModule(name string) Module {
if !isValidSegment(name, minModuleSeg) {
panic(ErrInvalidModule)
}
return Module(name)
}
// MakeCode genera un Code perteneciente a este módulo.
// Ejemplo: authMod.MakeCode("user_not_found") -> "auth::user_not_found"
func (m Module) MakeCode(code string) Code {
if !isValidSegment(code, minCodeSeg) {
panic(ErrInvalidCode)
}
return Code(string(m) + ModuleSep + code)
}
func ValidateModule(module string) error {
if !isValidSegment(module, minModuleSeg) {
return ErrInvalidModule
}
return nil
}

Powered by TurnKey Linux.