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.
384 lines
13 KiB
384 lines
13 KiB
/* ——————————————————————————————————————————————————————————————————————————
|
|
Proyecto goat
|
|
—————————————————————————————————————————————————————————————————————————————
|
|
Fichero code.go
|
|
Package errs
|
|
Autor Juan V. Navarro juanvnl@activething.com
|
|
Creado 02/02/2026
|
|
—————————————————————————————————————————————————————————————————————————————
|
|
|
|
LICENSES AND TERMS OF USE
|
|
-------------------------
|
|
|
|
This software is licensed under the Elastic License v2.0 (the "License").
|
|
For full terms and additional information regarding permitted and prohibited
|
|
uses, please visit:
|
|
https://activething.com/ATGO/licenses
|
|
|
|
You may use, copy, modify, and redistribute this software internally within
|
|
your organization for any purpose, including research, development, and
|
|
testing, subject to the terms of this License.
|
|
|
|
You may NOT, however, use, provide, distribute, or make this software
|
|
available to any third party as part of a hosted service, SaaS offering, or
|
|
commercial product without first obtaining a commercial license from
|
|
Active Thing.
|
|
|
|
You may combine this software with other code, provided that such
|
|
combination does not circumvent the restrictions of this License.
|
|
|
|
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 NON-INFRINGEMENT. In no event shall
|
|
the authors or copyright holders be liable for any claim, damages, or other
|
|
liability arising from the use of this software.
|
|
|
|
—————————————————————————————————————————————————————————————————————————————
|
|
|
|
Web : activething.com | activething.com/goat
|
|
git : g.activething.com | github.com/activething/goat
|
|
Correo : dev@activething.com
|
|
|
|
—————————————————————————————————————————————————————————————————————————————
|
|
No deseo caminar sobre el agua", dijo Siddhartha.
|
|
Que los antiguos chamanes se contenten con tales habilidades.
|
|
—— Hermann Hesse, Siddhartha
|
|
—————————————————————————————————————————————————————————————————————————————
|
|
Copyright (c) 2026 Active Thing
|
|
————————————————————————————————————————————————————————————————————————————— */
|
|
|
|
package errs
|
|
|
|
import (
|
|
"core/btes"
|
|
"errors"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
// Definición de longitudes mínimas para la estructura "m::e".
|
|
// minCodeSeg: Longitud mínima de un segmento de error (ej. "e").
|
|
minCodeSeg = 1
|
|
// minCodeLength: Longitud mínima total (módulo + separador + error).
|
|
minCodeLength = minModuleSeg + lenModuleSep + minCodeSeg
|
|
|
|
wordSep = '_'
|
|
codeSepChar = '.'
|
|
CodeSep = string(codeSepChar)
|
|
lenCodeSep = 1
|
|
)
|
|
|
|
// Code representa un identificador único de error con estructura jerárquica y tipada.
|
|
//
|
|
// Formato: "modulo::categoria.subcategoria.error"
|
|
//
|
|
// El tipo Code es inmutable y está diseñado para ser ligero y eficiente en comparaciones.
|
|
// Actúa como la clave primaria para la identificación de errores en sistemas distribuidos.
|
|
//
|
|
// Reglas de Formato:
|
|
// 1. Módulo (Raíz): Obligatorio. Define el namespace (ej. "auth", "db").
|
|
// 2. Separador de Módulo (::): Separa estrictamente el origen del contexto.
|
|
// 3. Jerarquía (Cuerpo): Segmentos separados por puntos '.' que denotan especificidad.
|
|
//
|
|
// Restricciones de Caracteres:
|
|
// - Alfanuméricos: Solo minúsculas [a-z] y números [0-9].
|
|
// - Palabras: Se permite guion bajo '_' como separador de palabras (snake_case).
|
|
// - Estructurales: Se permite punto '.' para jerarquía y '::' para módulo.
|
|
// - Prohibido: Espacios, mayúsculas, guiones medios, o símbolos especiales.
|
|
type Code string
|
|
|
|
// Sub crea un nuevo código hijo añadiendo un segmento más específico al actual.
|
|
// Es la forma canónica de extender errores (especialización).
|
|
//
|
|
// Ejemplo:
|
|
//
|
|
// base := Code("auth::login")
|
|
// child := base.Sub("failed") // Resultado: "auth::login.failed"
|
|
//
|
|
// Panics:
|
|
//
|
|
// Lanza panic(ErrInvalidCode) si el segmento proporcionado no es válido
|
|
// (vacío, caracteres ilegales o empieza/termina con separadores).
|
|
func (c Code) Sub(segment string) Code {
|
|
// 1. Chequeo de integridad rápido (O(1))
|
|
// En lugar de re-validar todo el string, solo aseguramos que no esté vacío
|
|
// y que tenga el separador de módulo.
|
|
// confiamos que el code haya sido validado a traves
|
|
// de la creación por parte del modulo
|
|
if c == "" {
|
|
panic(ErrInvalidCodeLen)
|
|
}
|
|
if !isValidSegment(segment, minCodeSeg) {
|
|
panic(ErrInvalidCode)
|
|
}
|
|
return c + Code(CodeSep+segment)
|
|
}
|
|
|
|
// Module extrae el espacio de nombres (namespace) del código.
|
|
//
|
|
// Ejemplo:
|
|
//
|
|
// Code("db::conn.timeout").Module() -> "db", nil
|
|
//
|
|
// Retorna:
|
|
// - Module: El módulo extraído.
|
|
// - error: ErrInvalidModule si el código no contiene el separador "::".
|
|
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 código del nivel jerárquico inmediatamente superior.
|
|
// Se utiliza para agrupar errores por categorías generales.
|
|
//
|
|
// Comportamiento:
|
|
// - Si tiene jerarquía ("a::b.c") -> recorta el último segmento ("a::b").
|
|
// - Si es un error base ("a::b") -> devuelve string vacío "" (no tiene padre dentro del código).
|
|
//
|
|
// 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 extrae el segmento más específico (la hoja) del árbol de error.
|
|
// Es útil para mensajes de error cortos o interfaces de usuario.
|
|
//
|
|
// Ejemplos:
|
|
//
|
|
// "auth::user.login.failed" -> "failed"
|
|
// "auth::user" -> "user"
|
|
func (c Code) Leaf() string {
|
|
s := string(c)
|
|
// 1. Buscamos el último punto (jerarquía interna)
|
|
if i := strings.LastIndex(s, CodeSep); i >= 0 {
|
|
return s[i+1:]
|
|
}
|
|
// 2. Si no hay puntos, buscamos el separador de módulo
|
|
if i := strings.LastIndex(s, ModuleSep); i >= 0 {
|
|
return s[i+lenModuleSep:]
|
|
}
|
|
// Fallback: Si el formato está roto, devolvemos todo
|
|
return s
|
|
}
|
|
|
|
// Error implementa la interfaz error estándar de Go.
|
|
// Devuelve la representación string completa del código.
|
|
func (c Code) Error() string {
|
|
return string(c)
|
|
}
|
|
|
|
// String implementa la interfaz fmt.Stringer.
|
|
func (c Code) String() string {
|
|
return string(c)
|
|
}
|
|
|
|
// IsValid comprueba que el código es un código de error
|
|
// valido
|
|
func (c Code) IsValid() bool {
|
|
return ValidateCode(string(c)) == nil
|
|
}
|
|
|
|
// Is comprueba la identidad estricta contra otro error.
|
|
// Soporta "error wrapping" estándar de Go 1.13+.
|
|
func (c Code) Is(target error) bool {
|
|
var t Code
|
|
return errors.As(target, &t) && c == t
|
|
}
|
|
|
|
// IsDescendantOf verifica si el código actual es un subtipo estricto del error padre.
|
|
// Devuelve true SOLO si es un hijo/nieto, pero false si son iguales.
|
|
//
|
|
// Diferencia con EqualOrDescendantOf:
|
|
// - IsDescendantOf("a::b", "a::b") -> false
|
|
// - EqualOrDescendantOf("a::b", "a::b") -> 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
|
|
}
|
|
// Verificamos prefijo con separador para evitar falsos positivos
|
|
// (ej: "user_group" no es descendiente de "user")
|
|
return strings.HasPrefix(cd, ps+CodeSep)
|
|
}
|
|
|
|
// EqualOrDescendantOf verifica pertenencia a una familia de errores.
|
|
// Devuelve true si el código es igual al target O si desciende de él.
|
|
//
|
|
// Uso recomendado:
|
|
//
|
|
// if errCode.EqualOrDescendantOf(ErrDatabase) { ... }
|
|
func (c Code) EqualOrDescendantOf(err error) bool {
|
|
var t Code
|
|
if !errors.As(err, &t) {
|
|
return false
|
|
}
|
|
sc, st := string(c), string(t)
|
|
// 1. Identidad
|
|
if sc == st {
|
|
return true
|
|
}
|
|
// 2. Linaje (asegurando límite de palabra con CodeSep)
|
|
return strings.HasPrefix(sc, st+CodeSep)
|
|
}
|
|
|
|
// Parts descompone el código en sus elementos atómicos.
|
|
//
|
|
// Retorna:
|
|
// - []string: Slice donde [0] es el Módulo y [1..n] son los segmentos.
|
|
// - error: ErrInvalidModule o ErrInvalidCode si el formato es incorrecto.
|
|
//
|
|
// Ejemplos:
|
|
//
|
|
// "auth::failed" → ["auth", "failed"], nil
|
|
// "auth::failed.invalid_arg" → ["auth", "failed", "invalid_arg"], nil
|
|
func (c Code) Parts() ([]string, error) {
|
|
s := string(c)
|
|
n := len(s)
|
|
|
|
moduleEnd := strings.Index(s, ModuleSep)
|
|
|
|
if moduleEnd == -1 {
|
|
return nil, ErrInvalidModule // Falta "::"
|
|
}
|
|
|
|
if moduleEnd+lenModuleSep >= n {
|
|
return nil, ErrInvalidCode // Solo módulo "mod::"
|
|
}
|
|
|
|
var segments []string
|
|
// Añadir módulo
|
|
segments = append(segments, s[:moduleEnd])
|
|
|
|
// Añadir segmentos
|
|
start := moduleEnd + lenModuleSep
|
|
for i := start; i <= n; i++ {
|
|
// Cortamos al final del string o al encontrar un punto
|
|
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 realiza una validación exhaustiva y optimizada del formato del código.
|
|
//
|
|
// Criterios de Validez:
|
|
// 1. Estructura Global: Debe ser "modulo::error" (mínimo 4 chars: "m::e").
|
|
// 2. Unicidad de Módulo: Solo puede haber un separador "::".
|
|
// 3. Extremos: No puede empezar ni terminar con '.', ':', '-', '_'.
|
|
// 4. Caracteres: Solo [a-z], [0-9], y separadores permitidos ('.', '::', '_').
|
|
// 5. Coherencia:
|
|
// - '.' solo permitido DESPUÉS del módulo '::'.
|
|
// - No se permiten separadores consecutivos (ej: "..", "._", "::.").
|
|
// - '_' (wordSep) no puede ser adyacente a otros separadores.
|
|
//
|
|
// Retornos:
|
|
// - nil: El código es válido.
|
|
// - ErrInvalidCodeLen: Longitud insuficiente.
|
|
// - ErrInvalidModule: Falta el separador de módulo.
|
|
// - ErrInvalidCodeChar: Caracteres ilegales o mala estructura.
|
|
func ValidateCode(code string) error {
|
|
ln := len(code)
|
|
if ln < minCodeLength {
|
|
return ErrInvalidCodeLen
|
|
}
|
|
// Optimización: Verificación rápida de extremos
|
|
if !btes.ASCII.IsAlphaNumLower(code[0]) || !btes.ASCII.IsAlphaNumLower(code[ln-1]) {
|
|
return ErrInvalidCodeChar
|
|
}
|
|
|
|
hasModuleSep := false
|
|
prevWasSep := false
|
|
|
|
for i := 0; i < ln; i++ {
|
|
ch := code[i]
|
|
|
|
// 1. Caracteres Alfanuméricos (Válidos, reinician flag de separador)
|
|
if btes.ASCII.IsAlphaNumLower(ch) {
|
|
prevWasSep = false
|
|
continue
|
|
}
|
|
|
|
// 2. Separador de palabras ('_')
|
|
if ch == wordSep {
|
|
if prevWasSep {
|
|
return ErrInvalidCodeChar // Ej: "._", "::_"
|
|
}
|
|
prevWasSep = false // NOTA: Esto permite "__". Si se desea prohibir, cambiar a true.
|
|
continue
|
|
}
|
|
|
|
// 3. Separador de Módulo ('::')
|
|
if ch == moduleSepChar {
|
|
// Violaciones: Ya existe módulo, estamos al final, no es '::' doble, o viene de otro separador
|
|
if hasModuleSep || i+1 >= ln || code[i+1] != moduleSepChar || prevWasSep {
|
|
return ErrInvalidCodeChar
|
|
}
|
|
hasModuleSep = true
|
|
prevWasSep = true
|
|
i++ // Avanzamos el índice para saltar el segundo ':'
|
|
continue
|
|
}
|
|
|
|
// 4. Separador de Jerarquía ('.')
|
|
if ch == codeSepChar {
|
|
// Violaciones: No hay módulo aún, o viene de otro separador
|
|
if !hasModuleSep || prevWasSep {
|
|
return ErrInvalidCodeChar
|
|
}
|
|
prevWasSep = true
|
|
continue
|
|
}
|
|
|
|
// 5. Cualquier otro carácter
|
|
return ErrInvalidCodeChar
|
|
}
|
|
|
|
if !hasModuleSep {
|
|
return ErrInvalidModule
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// isValidSegment verifica si un segmento individual (nombre de módulo o parte del error)
|
|
// cumple con las reglas: alfanumérico, minúsculas, permite '_' interno.
|
|
func isValidSegment(seg string, minLen int) bool {
|
|
ln := len(seg)
|
|
if ln < minLen {
|
|
return false
|
|
}
|
|
|
|
// Primer y último char deben ser alfanuméricos (no '_')
|
|
if !(btes.ASCII.IsAlphaNumLower(seg[0]) && btes.ASCII.IsAlphaNumLower(seg[ln-1])) {
|
|
return false
|
|
}
|
|
// Cuerpo del segmento
|
|
for i := 0; i < ln; i++ {
|
|
ch := seg[i]
|
|
if btes.ASCII.IsAlphaNumLower(ch) || ch == wordSep {
|
|
continue
|
|
}
|
|
return false
|
|
}
|
|
return true
|
|
}
|