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

8 months ago
/* ——————————————————————————————————————————————————————————————————————————
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 (
8 months ago
// 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).
8 months ago
minCodeLength = minModuleSeg + lenModuleSep + minCodeSeg
wordSep = '_'
codeSepChar = '.'
CodeSep = string(codeSepChar)
lenCodeSep = 1
)
8 months ago
// Code representa un identificador único de error con estructura jerárquica y tipada.
8 months ago
//
8 months ago
// Formato: "modulo::categoria.subcategoria.error"
8 months ago
//
8 months ago
// 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.
8 months ago
type Code string
8 months ago
// 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).
8 months ago
func (c Code) Sub(segment string) Code {
8 months ago
// 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)
}
8 months ago
if !isValidSegment(segment, minCodeSeg) {
panic(ErrInvalidCode)
}
return c + Code(CodeSep+segment)
}
8 months ago
// 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 "::".
8 months ago
func (c Code) Module() (Module, error) {
s := string(c)
if i := strings.Index(s, ModuleSep); i != -1 {
return Module(s[:i]), nil
}
return "", ErrInvalidModule
}
8 months ago
// 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).
8 months ago
//
// 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 ""
}
8 months ago
// 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.
8 months ago
//
// 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:]
}
8 months ago
// Fallback: Si el formato está roto, devolvemos todo
8 months ago
return s
}
8 months ago
// Error implementa la interfaz error estándar de Go.
// Devuelve la representación string completa del código.
8 months ago
func (c Code) Error() string {
return string(c)
}
8 months ago
// String implementa la interfaz fmt.Stringer.
8 months ago
func (c Code) String() string {
return string(c)
}
8 months ago
// 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+.
8 months ago
func (c Code) Is(target error) bool {
var t Code
return errors.As(target, &t) && c == t
}
8 months ago
// 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.
8 months ago
//
8 months ago
// Diferencia con EqualOrDescendantOf:
// - IsDescendantOf("a::b", "a::b") -> false
// - EqualOrDescendantOf("a::b", "a::b") -> true
8 months ago
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
}
8 months ago
// Verificamos prefijo con separador para evitar falsos positivos
// (ej: "user_group" no es descendiente de "user")
8 months ago
return strings.HasPrefix(cd, ps+CodeSep)
}
8 months ago
// EqualOrDescendantOf verifica pertenencia a una familia de errores.
// Devuelve true si el código es igual al target O si desciende de él.
8 months ago
//
8 months ago
// Uso recomendado:
8 months ago
//
8 months ago
// if errCode.EqualOrDescendantOf(ErrDatabase) { ... }
8 months ago
func (c Code) EqualOrDescendantOf(err error) bool {
var t Code
if !errors.As(err, &t) {
return false
}
sc, st := string(c), string(t)
8 months ago
// 1. Identidad
8 months ago
if sc == st {
return true
}
8 months ago
// 2. Linaje (asegurando límite de palabra con CodeSep)
8 months ago
return strings.HasPrefix(sc, st+CodeSep)
}
8 months ago
// 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.
8 months ago
//
// Ejemplos:
//
8 months ago
// "auth::failed" → ["auth", "failed"], nil
// "auth::failed.invalid_arg" → ["auth", "failed", "invalid_arg"], nil
8 months ago
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 {
8 months ago
return nil, ErrInvalidCode // Solo módulo "mod::"
8 months ago
}
var segments []string
8 months ago
// Añadir módulo
8 months ago
segments = append(segments, s[:moduleEnd])
8 months ago
// Añadir segmentos
8 months ago
start := moduleEnd + lenModuleSep
for i := start; i <= n; i++ {
8 months ago
// Cortamos al final del string o al encontrar un punto
8 months ago
if i == n || s[i] == CodeSep[0] {
segment := s[start:i]
8 months ago
if len(segment) > 0 {
8 months ago
segments = append(segments, segment)
}
start = i + 1
}
}
return segments, nil
}
8 months ago
// ValidateCode realiza una validación exhaustiva y optimizada del formato del código.
8 months ago
//
8 months ago
// 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.
8 months ago
//
8 months ago
// Retornos:
// - nil: El código es válido.
// - ErrInvalidCodeLen: Longitud insuficiente.
// - ErrInvalidModule: Falta el separador de módulo.
// - ErrInvalidCodeChar: Caracteres ilegales o mala estructura.
8 months ago
func ValidateCode(code string) error {
ln := len(code)
if ln < minCodeLength {
return ErrInvalidCodeLen
}
8 months ago
// Optimización: Verificación rápida de extremos
8 months ago
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]
8 months ago
// 1. Caracteres Alfanuméricos (Válidos, reinician flag de separador)
8 months ago
if btes.ASCII.IsAlphaNumLower(ch) {
prevWasSep = false
continue
}
8 months ago
// 2. Separador de palabras ('_')
8 months ago
if ch == wordSep {
if prevWasSep {
8 months ago
return ErrInvalidCodeChar // Ej: "._", "::_"
8 months ago
}
8 months ago
prevWasSep = false // NOTA: Esto permite "__". Si se desea prohibir, cambiar a true.
8 months ago
continue
}
8 months ago
// 3. Separador de Módulo ('::')
8 months ago
if ch == moduleSepChar {
8 months ago
// Violaciones: Ya existe módulo, estamos al final, no es '::' doble, o viene de otro separador
8 months ago
if hasModuleSep || i+1 >= ln || code[i+1] != moduleSepChar || prevWasSep {
return ErrInvalidCodeChar
}
hasModuleSep = true
prevWasSep = true
8 months ago
i++ // Avanzamos el índice para saltar el segundo ':'
8 months ago
continue
}
8 months ago
// 4. Separador de Jerarquía ('.')
8 months ago
if ch == codeSepChar {
8 months ago
// Violaciones: No hay módulo aún, o viene de otro separador
8 months ago
if !hasModuleSep || prevWasSep {
return ErrInvalidCodeChar
}
prevWasSep = true
continue
}
8 months ago
// 5. Cualquier otro carácter
8 months ago
return ErrInvalidCodeChar
}
if !hasModuleSep {
return ErrInvalidModule
}
return nil
}
8 months ago
// isValidSegment verifica si un segmento individual (nombre de módulo o parte del error)
// cumple con las reglas: alfanumérico, minúsculas, permite '_' interno.
8 months ago
func isValidSegment(seg string, minLen int) bool {
ln := len(seg)
if ln < minLen {
return false
}
8 months ago
// Primer y último char deben ser alfanuméricos (no '_')
8 months ago
if !(btes.ASCII.IsAlphaNumLower(seg[0]) && btes.ASCII.IsAlphaNumLower(seg[ln-1])) {
return false
}
8 months ago
// Cuerpo del segmento
8 months ago
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.