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.
499 lines
14 KiB
499 lines
14 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 (
|
|
// 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)
|
|
// 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:]
|
|
}
|
|
// Si por alguna razón no hay separadores, devolvemos el string completo
|
|
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)
|
|
}
|
|
|
|
// EqualOrDescendantOf 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".EqualOrDescendantOf("auth::login") -> true (descendiente)
|
|
// "auth::login".EqualOrDescendantOf("auth::login") -> true (identidad)
|
|
// "auth::login".EqualOrDescendantOf("auth::logout") -> false
|
|
func (c Code) EqualOrDescendantOf(err error) bool {
|
|
var t Code
|
|
if !errors.As(err, &t) {
|
|
return false
|
|
}
|
|
sc, st := string(c), string(t)
|
|
// 1. Verificación de identidad
|
|
if sc == st {
|
|
return true
|
|
}
|
|
// 2. Verificación de linaje
|
|
// Añadimos CodeSep (".") para garantizar que "user" no sea padre de "users"
|
|
return strings.HasPrefix(sc, st+CodeSep)
|
|
}
|
|
|
|
// 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)
|
|
|
|
// Buscar separador de módulo
|
|
moduleEnd := strings.Index(s, ModuleSep)
|
|
|
|
// Validaciones
|
|
if moduleEnd == -1 {
|
|
return nil, ErrInvalidModule // Falta "::"
|
|
}
|
|
|
|
if moduleEnd+lenModuleSep >= n {
|
|
return nil, ErrInvalidCode // Solo módulo sin código (ej: "auth::")
|
|
}
|
|
|
|
// Extraer módulo
|
|
var segments []string
|
|
segments = append(segments, s[:moduleEnd])
|
|
|
|
// Extraer segmentos de código
|
|
start := moduleEnd + lenModuleSep
|
|
for i := start; i <= n; i++ {
|
|
if i == n || s[i] == CodeSep[0] {
|
|
segment := s[start:i]
|
|
if len(segment) > 0 { // Evitar segmentos vacíos
|
|
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)
|
|
// 1. Verificar longitud mínima
|
|
if ln < minCodeLength {
|
|
return ErrInvalidCodeLen
|
|
}
|
|
// No puede empezar/terminar con '.', ':', '_'
|
|
if !btes.ASCII.IsAlphaNumLower(code[0]) || !btes.ASCII.IsAlphaNumLower(code[ln-1]) {
|
|
return ErrInvalidCodeChar
|
|
}
|
|
// 3. Validar estructura interna
|
|
hasModuleSep := false
|
|
prevWasSep := false
|
|
|
|
for i := 0; i < ln; i++ {
|
|
ch := code[i]
|
|
|
|
// Caracteres alfanuméricos (siempre permitidos)
|
|
if btes.ASCII.IsAlphaNumLower(ch) {
|
|
prevWasSep = false
|
|
continue
|
|
}
|
|
|
|
// WordSep
|
|
// ✅ FIX: Solo permitidos si NO venimos de un separador
|
|
if ch == wordSep {
|
|
if prevWasSep {
|
|
return ErrInvalidCodeChar // No permitir "::_" o ".-" o "::-"
|
|
}
|
|
prevWasSep = false
|
|
continue
|
|
}
|
|
|
|
// Separador de módulo '::'
|
|
if ch == moduleSepChar {
|
|
// Ya encontramos el módulo antes, no puede haber otro ':'
|
|
if hasModuleSep {
|
|
return ErrInvalidCodeChar
|
|
}
|
|
// Verificar que es '::', no ':' suelto
|
|
if i+1 >= ln || code[i+1] != moduleSepChar {
|
|
return ErrInvalidCodeChar
|
|
}
|
|
// No puede venir de otro separador
|
|
if prevWasSep {
|
|
return ErrInvalidCodeChar
|
|
}
|
|
hasModuleSep = true
|
|
prevWasSep = true
|
|
i++ // Saltar el segundo ':'
|
|
continue
|
|
}
|
|
|
|
// Separador de jerarquía '.'
|
|
if ch == codeSepChar {
|
|
// Regla: '.' solo después de '::'
|
|
if !hasModuleSep {
|
|
return ErrInvalidCodeChar
|
|
}
|
|
// No puede haber separadores consecutivos
|
|
if prevWasSep {
|
|
return ErrInvalidCodeChar
|
|
}
|
|
prevWasSep = true
|
|
continue
|
|
}
|
|
|
|
// Carácter inválido
|
|
return ErrInvalidCodeChar
|
|
}
|
|
|
|
// Válido solo si encontramos el separador de módulo
|
|
if !hasModuleSep {
|
|
return ErrInvalidModule
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func ValidateCode2(code string) error {
|
|
ln := len(code)
|
|
// 1. Verificar longitud mínima
|
|
if ln < minCodeLength {
|
|
return ErrInvalidCodeLen
|
|
}
|
|
// No puede empezar/terminar con '.', ':', '_'
|
|
if !isValidChar(code[0]) || !isValidChar(code[ln-1]) {
|
|
return ErrInvalidCodeChar
|
|
}
|
|
// 3. Validar estructura interna
|
|
hasModuleSep := false
|
|
prevWasSep := false
|
|
|
|
for i := 0; i < ln; i++ {
|
|
ch := code[i]
|
|
|
|
// Caracteres alfanuméricos (siempre permitidos)
|
|
if isValidChar(ch) {
|
|
prevWasSep = false
|
|
continue
|
|
}
|
|
|
|
// WordSep
|
|
// ✅ FIX: Solo permitidos si NO venimos de un separador
|
|
if ch == wordSep {
|
|
if prevWasSep {
|
|
return ErrInvalidCodeChar // No permitir "::_" o ".-" o "::-"
|
|
}
|
|
prevWasSep = false
|
|
continue
|
|
}
|
|
|
|
// Separador de módulo '::'
|
|
if ch == moduleSepChar {
|
|
// Ya encontramos el módulo antes, no puede haber otro ':'
|
|
if hasModuleSep {
|
|
return ErrInvalidCodeChar
|
|
}
|
|
// Verificar que es '::', no ':' suelto
|
|
if i+1 >= ln || code[i+1] != moduleSepChar {
|
|
return ErrInvalidCodeChar
|
|
}
|
|
// No puede venir de otro separador
|
|
if prevWasSep {
|
|
return ErrInvalidCodeChar
|
|
}
|
|
hasModuleSep = true
|
|
prevWasSep = true
|
|
i++ // Saltar el segundo ':'
|
|
continue
|
|
}
|
|
|
|
// Separador de jerarquía '.'
|
|
if ch == codeSepChar {
|
|
// Regla: '.' solo después de '::'
|
|
if !hasModuleSep {
|
|
return ErrInvalidCodeChar
|
|
}
|
|
// No puede haber separadores consecutivos
|
|
if prevWasSep {
|
|
return ErrInvalidCodeChar
|
|
}
|
|
prevWasSep = true
|
|
continue
|
|
}
|
|
|
|
// Carácter inválido
|
|
return ErrInvalidCodeChar
|
|
}
|
|
|
|
// Válido solo si encontramos el separador de módulo
|
|
if !hasModuleSep {
|
|
return ErrInvalidModule
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func ValidateCode3(code string) error {
|
|
ln := len(code)
|
|
if ln < minCodeLength {
|
|
return ErrInvalidCodeLen
|
|
}
|
|
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]
|
|
|
|
if btes.ASCII.IsAlphaNumLower(ch) {
|
|
prevWasSep = false
|
|
continue
|
|
}
|
|
|
|
if ch == wordSep {
|
|
if prevWasSep {
|
|
return ErrInvalidCodeChar
|
|
}
|
|
prevWasSep = false
|
|
continue
|
|
}
|
|
|
|
if ch == moduleSepChar {
|
|
if hasModuleSep || i+1 >= ln || code[i+1] != moduleSepChar || prevWasSep {
|
|
return ErrInvalidCodeChar
|
|
}
|
|
hasModuleSep = true
|
|
prevWasSep = true
|
|
i++ // salto del segundo ':'
|
|
continue
|
|
}
|
|
|
|
if ch == codeSepChar {
|
|
if !hasModuleSep || prevWasSep {
|
|
return ErrInvalidCodeChar
|
|
}
|
|
prevWasSep = true
|
|
continue
|
|
}
|
|
|
|
// Cualquier otro char inválido
|
|
return ErrInvalidCodeChar
|
|
}
|
|
|
|
if !hasModuleSep {
|
|
return ErrInvalidModule
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func isValidSegment(seg string, minLen int) bool {
|
|
ln := len(seg)
|
|
if ln < minLen {
|
|
return false
|
|
}
|
|
|
|
// Usamos tu variable centralizada isValidChar (que es IsAlphaNumLower)
|
|
// Los segmentos no pueden empezar ni terminar con '_'
|
|
if !(btes.ASCII.IsAlphaNumLower(seg[0]) && btes.ASCII.IsAlphaNumLower(seg[ln-1])) {
|
|
return false
|
|
}
|
|
for i := 0; i < ln; i++ {
|
|
ch := seg[i]
|
|
if btes.ASCII.IsAlphaNumLower(ch) || ch == wordSep {
|
|
continue
|
|
}
|
|
return false
|
|
}
|
|
return true
|
|
}
|