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

/* ——————————————————————————————————————————————————————————————————————————
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
}

Powered by TurnKey Linux.