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