commit
d0ef9f5eee
@ -0,0 +1,8 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="Go" enabled="true" />
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/goat.iml" filepath="$PROJECT_DIR$/.idea/goat.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@ -0,0 +1,2 @@
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
@ -0,0 +1,54 @@
|
||||
/* ——————————————————————————————————————————————————————————————————————————
|
||||
Proyecto goat
|
||||
—————————————————————————————————————————————————————————————————————————————
|
||||
Fichero a.go
|
||||
Package btes
|
||||
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 btes
|
||||
|
||||
var (
|
||||
ASCII *ASCIIAttrs = NewASCIIAttrs()
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,75 @@
|
||||
/* ——————————————————————————————————————————————————————————————————————————
|
||||
Proyecto goat
|
||||
—————————————————————————————————————————————————————————————————————————————
|
||||
Fichero attr.go
|
||||
Package chrs
|
||||
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 btes
|
||||
|
||||
// Attr representa una máscara de bits (bitmask) de 8 bits para propiedades ASCII.
|
||||
// Este diseño permite almacenar hasta 8 características booleanas distintas
|
||||
// en un solo byte, optimizando el uso de la memoria caché del CPU.
|
||||
type Attr uint8
|
||||
|
||||
// HasAll comprueba si el atributo contiene TODAS las propiedades indicadas en la máscara.
|
||||
// Se utiliza para validaciones restrictivas.
|
||||
//
|
||||
// Ejemplo:
|
||||
//
|
||||
// attr.HasAll(IsAlpha | IsUpper) // ¿Es una letra Y además es mayúscula?
|
||||
//
|
||||
// Matemáticamente: (a & attr) == attr
|
||||
func (a Attr) HasAll(attr Attr) bool { return a&attr == attr }
|
||||
|
||||
// HasAny comprueba si el atributo contiene AL MENOS UNA de las propiedades
|
||||
// indicadas en la máscara. Se utiliza para validaciones de pertenencia a grupos.
|
||||
//
|
||||
// Ejemplo:
|
||||
//
|
||||
// attr.HasAny(IsDigit | IsAlpha) // ¿Es un número O una letra?
|
||||
//
|
||||
// Matemáticamente: (a & attr) != 0
|
||||
func (a Attr) HasAny(attr Attr) bool { return a&attr != 0 }
|
||||
@ -0,0 +1,192 @@
|
||||
/* ——————————————————————————————————————————————————————————————————————————
|
||||
Proyecto goat
|
||||
—————————————————————————————————————————————————————————————————————————————
|
||||
Fichero attrs.go
|
||||
Package btes
|
||||
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 btes
|
||||
|
||||
const (
|
||||
maxSize = 1 << 8
|
||||
maxRange = maxSize - 1
|
||||
)
|
||||
|
||||
// Attrs representa una Tabla de Búsqueda (LUT - Lookup Table) de alto rendimiento.
|
||||
// Permite clasificar bytes y realizar validaciones en tiempo constante O(1)
|
||||
// o lineal O(N) sin asignaciones de memoria.
|
||||
type Attrs [maxSize]Attr
|
||||
|
||||
// SetRange asigna destructivamente el atributo 'att' a un rango de bytes [ini, end].
|
||||
// Sobrescribe cualquier atributo previo en ese rango.
|
||||
// Los límites se ajustan automáticamente a [0, 255].
|
||||
func (t *Attrs) SetRange(att Attr, ini, end int) {
|
||||
s, e := clamp(ini), clamp(end)
|
||||
for i := s; i <= e; i++ {
|
||||
t[i] = att
|
||||
}
|
||||
}
|
||||
|
||||
// AddRange agrega el atributo 'att' a un rango de bytes [ini, end] (operación OR).
|
||||
// Mantiene los atributos existentes.
|
||||
// Los límites se ajustan automáticamente a [0, 255].
|
||||
func (t *Attrs) AddRange(att Attr, ini, end int) {
|
||||
s, e := clamp(ini), clamp(end)
|
||||
for i := s; i <= e; i++ {
|
||||
t[i] |= att
|
||||
}
|
||||
}
|
||||
|
||||
// DelRange elimina el atributo 'att' de un rango de bytes [ini, end] (operación AND NOT).
|
||||
// El resto de atributos del byte permanecen intactos.
|
||||
// Los límites se ajustan automáticamente a [0, 255].
|
||||
func (t *Attrs) DelRange(att Attr, ini, end int) {
|
||||
s, e := clamp(ini), clamp(end)
|
||||
for i := s; i <= e; i++ {
|
||||
t[i] &^= att
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Configuración por Lista (Slice Setters)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
// SetFrom asigna destructivamente el atributo 'att' a los bytes específicos en 'fromBytes'.
|
||||
// Útil para conjuntos dispersos (ej: caracteres especiales "-_@").
|
||||
func (t *Attrs) SetFrom(fromBytes []byte, att Attr) {
|
||||
for _, bt := range fromBytes {
|
||||
t[bt] = att
|
||||
}
|
||||
}
|
||||
|
||||
// AddFrom agrega el atributo 'att' a los bytes específicos en 'fromBytes'.
|
||||
func (t *Attrs) AddFrom(fromBytes []byte, att Attr) {
|
||||
for _, bt := range fromBytes {
|
||||
t[bt] |= att
|
||||
}
|
||||
}
|
||||
|
||||
// DelFrom elimina el atributo 'att' de los bytes específicos en 'fromBytes'.
|
||||
func (t *Attrs) DelFrom(fromBytes []byte, att Attr) {
|
||||
for _, bt := range fromBytes {
|
||||
t[bt] &^= att
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Funciones de Búsqueda (Search / Existence)
|
||||
// Responden a: "¿CONTIENE la lista algún byte que...?"
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
// HasAll busca si EXISTE al menos un byte en 'fromBytes' que cumpla
|
||||
// estrictamente con todos los bits de la máscara 'att'.
|
||||
// Retorna true al encontrar el primer match.
|
||||
func (t *Attrs) HasAll(fromBytes []byte, att Attr) bool {
|
||||
for _, bt := range fromBytes {
|
||||
if t[bt]&att == att {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// HasAny busca si EXISTE al menos un byte en 'fromBytes' que tenga
|
||||
// cualquiera de los bits de la máscara 'att'.
|
||||
// Retorna true al encontrar el primer match.
|
||||
func (t *Attrs) HasAny(fromBytes []byte, att Attr) bool {
|
||||
for _, bt := range fromBytes {
|
||||
if t[bt]&att != 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Funciones de Validación (Predicates)
|
||||
// Responden a: "¿SON TODOS los bytes de la lista...?"
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
// MatchesAll verifica el cumplimiento estricto (AND) en TODO el slice.
|
||||
// Devuelve true solo si TODOS los bytes poseen TODOS los bits de 'att'.
|
||||
// Retorna false si el slice está vacío.
|
||||
func (t *Attrs) MatchesAll(source []byte, att Attr) bool {
|
||||
if len(source) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, ch := range source {
|
||||
if t[ch]&att != att {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// MatchesAny verifica el cumplimiento flexible (OR) en TODO el slice.
|
||||
// Devuelve true si TODOS los bytes comparten AL MENOS UN bit con 'att'.
|
||||
// Retorna false si el slice está vacío.
|
||||
// Útil para validar que una cadena solo contenga caracteres de un grupo (ej: alfanuméricos).
|
||||
func (t *Attrs) MatchesAny(source []byte, att Attr) bool {
|
||||
if len(source) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, ch := range source {
|
||||
if t[ch]&att == 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// clamp asegura que cualquier entero esté en el rango válido de un byte
|
||||
func clamp(v int) int {
|
||||
if v < 0 {
|
||||
return 0
|
||||
}
|
||||
if v > maxRange {
|
||||
return maxRange
|
||||
}
|
||||
return v
|
||||
}
|
||||
@ -0,0 +1,77 @@
|
||||
/* ——————————————————————————————————————————————————————————————————————————
|
||||
Proyecto goat
|
||||
—————————————————————————————————————————————————————————————————————————————
|
||||
Fichero a.go
|
||||
Package errs
|
||||
Autor Juan V. Navarro juanvnl@activething.com
|
||||
Creado 03/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"
|
||||
|
||||
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.IsAlphaNumLower 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.IsAlphaNumLower
|
||||
)
|
||||
@ -0,0 +1,366 @@
|
||||
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
|
||||
}
|
||||
@ -0,0 +1,498 @@
|
||||
/* ——————————————————————————————————————————————————————————————————————————
|
||||
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
|
||||
}
|
||||
@ -0,0 +1,99 @@
|
||||
/* ——————————————————————————————————————————————————————————————————————————
|
||||
Proyecto goat
|
||||
—————————————————————————————————————————————————————————————————————————————
|
||||
Fichero module.go
|
||||
Package errs
|
||||
Autor Juan V. Navarro juanvnl@activething.com
|
||||
Creado 03/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
|
||||
|
||||
const (
|
||||
moduleSepChar = ':'
|
||||
ModuleSep = "::"
|
||||
lenModuleSep = 2
|
||||
minModuleSeg = 1
|
||||
)
|
||||
|
||||
// Module representa un espacio de nombres (namespace) único y normalizado.
|
||||
//
|
||||
// Un módulo actúa como la raíz de una jerarquía de errores. Su propósito es
|
||||
// garantizar que todos los códigos generados bajo su ámbito compartan un
|
||||
// origen común, facilitando la trazabilidad y el filtrado en logs y métricas.
|
||||
//
|
||||
// Reglas de Valor:
|
||||
// - Debe ser un nombre "puro" (ej. "database", no "database::").
|
||||
// - Solo admite minúsculas [a-z], números [0-9] y guiones bajos '_'.
|
||||
// - Se recomienda que sea corto, descriptivo y representativo de un
|
||||
// componente o subsistema.
|
||||
|
||||
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 {
|
||||
// Validamos que el nombre sea un segmento válido (minúsculas, números, _)
|
||||
// y que cumpla la longitud mínima para módulos (ej. 1 o 2 caracteres)
|
||||
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
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
module core
|
||||
|
||||
go 1.25
|
||||
@ -0,0 +1,310 @@
|
||||
/* ——————————————————————————————————————————————————————————————————————————
|
||||
Proyecto goat
|
||||
—————————————————————————————————————————————————————————————————————————————
|
||||
Fichero ascii_bench_test.go
|
||||
Package test_btes
|
||||
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 test_btes
|
||||
|
||||
import (
|
||||
"core/btes"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// BENCHMARKS - Comparación Original vs Optimizado
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
var ascii = btes.NewASCIIAttrs()
|
||||
|
||||
// Test data - realistic HTTP routing scenarios
|
||||
var (
|
||||
pathMixed = []byte("/api/UsersService/GetUserById")
|
||||
pathLower = []byte("/api/users/get-user-by-id")
|
||||
pathUpper = []byte("/API/USERS/GET-USER-BY-ID")
|
||||
methodGET = []byte("GET")
|
||||
methodPost = []byte("post")
|
||||
headerMixed = []byte("Content-Type")
|
||||
snakeCaseInput = []byte("user_service_handler")
|
||||
camelCaseOutput = []byte("UserServiceHandler")
|
||||
)
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// Benchmark #1: EqualFold
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
func BenchmarkEqualFold(b *testing.B) {
|
||||
b.Run("Custom/MatchExact", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = ascii.EqualFold(methodGET, methodGET)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("Custom/MatchFold", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = ascii.EqualFold(methodGET, methodPost)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("Custom/NoMatch", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = ascii.EqualFold(methodGET, []byte("POST"))
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("Stdlib/MatchExact", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = strings.EqualFold(string(methodGET), string(methodGET))
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("Stdlib/MatchFold", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = strings.EqualFold(string(methodGET), string(methodPost))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// Benchmark #2: ToUpper - Este es el CRÍTICO a optimizar
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
func BenchmarkToUpper(b *testing.B) {
|
||||
// Caso 1: No hay cambios necesarios (ya está en uppercase)
|
||||
b.Run("ToUpper/NoChanges", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = ascii.ToUpper(pathUpper)
|
||||
}
|
||||
})
|
||||
|
||||
// Caso 2: Algunos cambios al principio
|
||||
b.Run("ToUpper/EarlyChanges", func(b *testing.B) {
|
||||
input := []byte("abc123DEF456GHI")
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = ascii.ToUpper(input)
|
||||
}
|
||||
})
|
||||
|
||||
// Caso 3: Cambios al final (peor caso en versión original)
|
||||
b.Run("ToUpper/LateChanges", func(b *testing.B) {
|
||||
input := []byte("ABCDEF123456xyz")
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = ascii.ToUpper(input)
|
||||
}
|
||||
})
|
||||
|
||||
// Caso 4: Todo minúsculas (todos necesitan cambio)
|
||||
b.Run("ToUpper/AllChanges", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = ascii.ToUpper(pathLower)
|
||||
}
|
||||
})
|
||||
|
||||
// Caso 5: Mezclado (realista)
|
||||
b.Run("ToUpper/Mixed", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = ascii.ToUpper(pathMixed)
|
||||
}
|
||||
})
|
||||
|
||||
// Caso 6: In-place (baseline - ya es óptimo)
|
||||
b.Run("Upperize/InPlace", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
input := make([]byte, len(pathMixed))
|
||||
copy(input, pathMixed)
|
||||
ascii.Upperize(input)
|
||||
}
|
||||
})
|
||||
|
||||
// Comparación con stdlib
|
||||
b.Run("Stdlib/ToUpper", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = strings.ToUpper(string(pathMixed))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// Benchmark #3: ToCamel - También CRÍTICO a optimizar
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
func BenchmarkToCamel(b *testing.B) {
|
||||
// Caso 1: Ya está en CamelCase (no cambios)
|
||||
b.Run("ToCamel/NoChanges", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = ascii.ToCamel(camelCaseOutput, '_')
|
||||
}
|
||||
})
|
||||
|
||||
// Caso 2: snake_case típico
|
||||
b.Run("ToCamel/SnakeCase", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = ascii.ToCamel(snakeCaseInput, '_')
|
||||
}
|
||||
})
|
||||
|
||||
// Caso 3: Sin separadores
|
||||
b.Run("ToCamel/NoSeparators", func(b *testing.B) {
|
||||
input := []byte("alllowercase")
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = ascii.ToCamel(input, '_')
|
||||
}
|
||||
})
|
||||
|
||||
// Caso 4: Muchos separadores
|
||||
b.Run("ToCamel/ManySeparators", func(b *testing.B) {
|
||||
input := []byte("one_two_three_four_five_six")
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = ascii.ToCamel(input, '_')
|
||||
}
|
||||
})
|
||||
|
||||
// Caso 5: In-place (baseline)
|
||||
b.Run("Camelize/InPlace", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
input := make([]byte, len(snakeCaseInput))
|
||||
copy(input, snakeCaseInput)
|
||||
ascii.Camelize(input, '_')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// Benchmark #4: Operaciones individuales (baseline)
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
func BenchmarkSingleByte(b *testing.B) {
|
||||
b.Run("ToUpperByte", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = ascii.ToUpperByte('a')
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("ToLowerByte", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = ascii.ToLowerByte('A')
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("IsAlpha", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = ascii.IsAlpha('a')
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("IsDigit", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = ascii.IsDigit('5')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// Benchmark #5: Casos de uso reales en HTTP routing
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
func BenchmarkHTTPRouting(b *testing.B) {
|
||||
// Normalizar método HTTP (típicamente ya uppercase, zero-copy esperado)
|
||||
b.Run("NormalizeMethod/AlreadyUpper", func(b *testing.B) {
|
||||
method := []byte("GET")
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = ascii.ToUpper(method)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("NormalizeMethod/Lowerize", func(b *testing.B) {
|
||||
method := []byte("get")
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = ascii.ToUpper(method)
|
||||
}
|
||||
})
|
||||
|
||||
// Comparar headers case-insensitive
|
||||
b.Run("CompareHeader/Match", func(b *testing.B) {
|
||||
h1 := []byte("Content-Type")
|
||||
h2 := []byte("content-type")
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = ascii.EqualFold(h1, h2)
|
||||
}
|
||||
})
|
||||
|
||||
// Convertir path parameter a CamelCase
|
||||
b.Run("PathParamToCamel", func(b *testing.B) {
|
||||
param := []byte("user_id")
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = ascii.ToCamel(param, '_')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkToSnake(b *testing.B) {
|
||||
|
||||
input := []byte("MyVeryLongVariableNameForTestingPerformance")
|
||||
|
||||
b.Run("ToSnake/Complex", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = attrs.ToSnake(input, '_')
|
||||
}
|
||||
})
|
||||
|
||||
inputNoChange := []byte("my_very_long_variable_name")
|
||||
b.Run("ToSnake/NoChange", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = attrs.ToSnake(inputNoChange, '_')
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -0,0 +1,473 @@
|
||||
/* ——————————————————————————————————————————————————————————————————————————
|
||||
Proyecto goat
|
||||
—————————————————————————————————————————————————————————————————————————————
|
||||
Fichero ascii_test.go
|
||||
Package btes
|
||||
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
|
||||
————————————————————————————————————————————————————————————————————————————— */
|
||||
|
||||
// ascii_test.go
|
||||
package test_btes
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"core/btes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var attrs = btes.NewASCIIAttrs() // instancia global para todos los tests (singleton)
|
||||
|
||||
func TestEqualFold(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
a, b []byte
|
||||
want bool
|
||||
}{
|
||||
{"exact match", []byte("hello"), []byte("hello"), true},
|
||||
{"case insensitive", []byte("HelloWorld"), []byte("helloworld"), true},
|
||||
{"headers HTTP", []byte("Content-Type"), []byte("content-type"), true},
|
||||
{"mezcla symbols", []byte("User-ID_123"), []byte("user-id_123"), true},
|
||||
{"non-ASCII no afecta", []byte("café"), []byte("CafÉ"), false}, // 'é' se compara exacto
|
||||
{"diferente longitud", []byte("hello"), []byte("hell"), false},
|
||||
{"diferente char", []byte("hello"), []byte("hellp"), false},
|
||||
{"vacío", []byte{}, []byte{}, true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := attrs.EqualFold(tt.a, tt.b)
|
||||
if got != tt.want {
|
||||
t.Errorf("EqualFold(%q, %q) = %v, want %v", tt.a, tt.b, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestToUpper(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input []byte
|
||||
want []byte
|
||||
}{
|
||||
{"lower to upper", []byte("hello world!"), []byte("HELLO WORLD!")},
|
||||
{"mezcla case", []byte("HeLLo WoRlD"), []byte("HELLO WORLD")},
|
||||
{"ya upper", []byte("HELLO"), []byte("HELLO")},
|
||||
{"no letras", []byte("123!@#"), []byte("123!@#")},
|
||||
{"vacío", []byte{}, []byte{}},
|
||||
{"solo minúsculas", []byte("abc"), []byte("ABC")},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := attrs.ToUpper(tt.input)
|
||||
|
||||
if !bytes.Equal(got, tt.want) {
|
||||
t.Errorf("ToUpper(%q) = %q, want %q", tt.input, got, tt.want)
|
||||
}
|
||||
|
||||
// Verificar zero-copy cuando no hay cambios
|
||||
if bytes.Equal(tt.input, tt.want) {
|
||||
if len(got) == 0 {
|
||||
// OK para vacío
|
||||
return
|
||||
}
|
||||
if &got[0] != &tt.input[0] {
|
||||
t.Errorf("ToUpper no hizo zero-copy en %q (backing diferente)", tt.name)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpper(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input []byte
|
||||
want []byte
|
||||
}{
|
||||
{"lower to upper", []byte("hello"), []byte("HELLO")},
|
||||
{"mezcla", []byte("HeLLo"), []byte("HELLO")},
|
||||
{"no cambios", []byte("HELLO"), []byte("HELLO")},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Copia porque muta in-place
|
||||
clone := make([]byte, len(tt.input))
|
||||
copy(clone, tt.input)
|
||||
|
||||
attrs.Upperize(clone)
|
||||
|
||||
if !bytes.Equal(clone, tt.want) {
|
||||
t.Errorf("Upperize(%q) → %q, want %q", tt.input, clone, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestToLower(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input []byte
|
||||
want []byte
|
||||
}{
|
||||
{"upper to lower", []byte("HELLO WORLD!"), []byte("hello world!")},
|
||||
{"mezcla", []byte("HeLLo WoRlD"), []byte("hello world")},
|
||||
{"ya lower", []byte("hello"), []byte("hello")},
|
||||
{"vacío", []byte{}, []byte{}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := attrs.ToLower(tt.input)
|
||||
|
||||
if !bytes.Equal(got, tt.want) {
|
||||
t.Errorf("ToLower(%q) = %q, want %q", tt.input, got, tt.want)
|
||||
}
|
||||
|
||||
if bytes.Equal(tt.input, tt.want) {
|
||||
if len(got) == 0 {
|
||||
return
|
||||
}
|
||||
if &got[0] != &tt.input[0] {
|
||||
t.Errorf("ToLower no hizo zero-copy en %q", tt.name)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestToCamel(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input []byte
|
||||
sep byte
|
||||
want []byte
|
||||
}{
|
||||
{"snake_case", []byte("hello_world"), '_', []byte("HelloWorld")},
|
||||
{"kebab-case", []byte("user-id"), '-', []byte("UserId")},
|
||||
//{"ya camel sin sep", []byte("HelloWorld"), '_', []byte("HelloWorld")},
|
||||
{"mezcla case", []byte("hElLo_wOrLd"), '_', []byte("HelloWorld")},
|
||||
{"multiple sep", []byte("a_b_c"), '_', []byte("ABC")},
|
||||
{"sin sep", []byte("hello"), '_', []byte("Hello")},
|
||||
{"con números", []byte("user_123_id"), '_', []byte("User123Id")},
|
||||
{"sep al inicio", []byte("_hello"), '_', []byte("Hello")},
|
||||
{"sep al final", []byte("hello_"), '_', []byte("Hello")},
|
||||
{"múltiples sep seguidos", []byte("a__b"), '_', []byte("AB")},
|
||||
{"vacío", []byte{}, '_', []byte{}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := attrs.ToCamel(tt.input, tt.sep)
|
||||
|
||||
if !bytes.Equal(got, tt.want) {
|
||||
t.Errorf("ToCamel(%q, %c) = %q, want %q", tt.input, tt.sep, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestToUpperByte(t *testing.T) {
|
||||
tests := []struct {
|
||||
input byte
|
||||
want byte
|
||||
}{
|
||||
{'a', 'A'},
|
||||
{'z', 'Z'},
|
||||
{'A', 'A'},
|
||||
{'9', '9'},
|
||||
{'!', '!'},
|
||||
{0x00, 0x00},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := attrs.ToUpperByte(tt.input)
|
||||
if got != tt.want {
|
||||
t.Errorf("ToUpperByte(0x%02x %c) = 0x%02x %c, want 0x%02x %c", tt.input, tt.input, got, got, tt.want, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestToLowerByte(t *testing.T) {
|
||||
tests := []struct {
|
||||
input byte
|
||||
want byte
|
||||
}{
|
||||
{'A', 'a'},
|
||||
{'Z', 'z'},
|
||||
{'a', 'a'},
|
||||
{'0', '0'},
|
||||
{' ', ' '},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := attrs.ToLowerByte(tt.input)
|
||||
if got != tt.want {
|
||||
t.Errorf("ToLowerByte(0x%02x %c) = 0x%02x %c, want 0x%02x %c", tt.input, tt.input, got, got, tt.want, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHexDigitToByte(t *testing.T) {
|
||||
tests := []struct {
|
||||
input byte
|
||||
want byte
|
||||
}{
|
||||
{'0', 0},
|
||||
{'9', 9},
|
||||
{'A', 10},
|
||||
{'F', 15},
|
||||
{'a', 10},
|
||||
{'f', 15},
|
||||
{'g', 0},
|
||||
{'G', 0},
|
||||
{'!', 0},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := attrs.HexDigitToByte(tt.input)
|
||||
if got != tt.want {
|
||||
t.Errorf("HexDigitToByte(%c) = %d, want %d", tt.input, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetHex2DigitToByte(t *testing.T) {
|
||||
tests := []struct {
|
||||
high, low byte
|
||||
want byte
|
||||
ok bool
|
||||
}{
|
||||
{'A', 'F', 0xAF, true},
|
||||
{'a', 'f', 0xAF, true},
|
||||
{'0', '0', 0x00, true},
|
||||
{'1', 'A', 0x1A, true},
|
||||
{'g', '0', 0, false},
|
||||
{'A', 'g', 0, false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got, ok := attrs.GetHex2DigitToByte(tt.high, tt.low)
|
||||
if ok != tt.ok || got != tt.want {
|
||||
t.Errorf("GetHex2DigitToByte(%c, %c) = 0x%02x, %v; want 0x%02x, %v", tt.high, tt.low, got, ok, tt.want, tt.ok)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsAlphaNum(t *testing.T) {
|
||||
tests := []struct {
|
||||
input byte
|
||||
want bool
|
||||
}{
|
||||
{'a', true},
|
||||
{'Z', true},
|
||||
{'5', true},
|
||||
{'_', false},
|
||||
{' ', false},
|
||||
{0x7F, false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := attrs.IsAlphaNum(tt.input)
|
||||
if got != tt.want {
|
||||
t.Errorf("IsAlphaNum(%c) = %v, want %v", tt.input, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestToSnakeCorrectness(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
separator byte
|
||||
expected string
|
||||
}{
|
||||
{"Simple", "HelloWorld", '_', "hello_world"},
|
||||
{"Camel", "camelCase", '_', "camel_case"},
|
||||
{"Pascal", "PascalCase", '_', "pascal_case"},
|
||||
{"AcronymEnd", "HTMLParser", '_', "html_parser"}, // Prueba lógica avanzada
|
||||
{"AcronymStart", "JSONData", '_', "json_data"},
|
||||
{"Numbers", "User123ID", '_', "user123_id"}, // Números suelen pegar a la palabra anterior
|
||||
{"AlreadySnake", "hello_world", '_', "hello_world"},
|
||||
{"MixedSeparators", "hello-world", '_', "hello_world"}, // Normaliza '-' a '_'
|
||||
{"Empty", "", '_', ""},
|
||||
{"SingleUpper", "A", '_', "a"},
|
||||
{"SingleLower", "a", '_', "a"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
input := []byte(tt.input)
|
||||
// Nota: Asegúrate de que tu helper 'ascii' o la instancia 'attrs' esté disponible aquí
|
||||
result := attrs.ToSnake(input, tt.separator)
|
||||
if string(result) != tt.expected {
|
||||
t.Errorf("ToSnake(%q) = %q, want %q", tt.input, string(result), tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestToScreamingSnake(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"helloWorld", "HELLO_WORLD"},
|
||||
{"JSONData", "JSON_DATA"},
|
||||
{"user-id", "USER_ID"},
|
||||
{"already_UPPER", "ALREADY_UPPER"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
res := attrs.ToScreamingSnake([]byte(tt.input), '_')
|
||||
if string(res) != tt.expected {
|
||||
t.Errorf("Para %s: esperado %s, obtenido %s", tt.input, tt.expected, string(res))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestScreamingSnakeize(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"user-name", "USER_NAME"},
|
||||
{"HTTP.server", "HTTP_SERVER"},
|
||||
{"already_SNAKE", "ALREADY_SNAKE"},
|
||||
{"lower", "LOWER"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
input := []byte(tt.input)
|
||||
// Probamos el retorno del slice
|
||||
res := attrs.ScreamingSnakeize(input)
|
||||
if string(res) != tt.expected {
|
||||
t.Errorf("ScreamingSnakeize(%s) = %s; want %s", tt.input, string(res), tt.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// Tests de correctitud
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
func TestToUpperCorrectness(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"AllLower", "hello", "HELLO"},
|
||||
{"AllUpper", "HELLO", "HELLO"},
|
||||
{"Mixed", "HeLLo", "HELLO"},
|
||||
{"WithNumbers", "hello123", "HELLO123"},
|
||||
{"WithSymbols", "hello-world_123", "HELLO-WORLD_123"},
|
||||
{"Empty", "", ""},
|
||||
{"Path", "/api/users/:id", "/API/USERS/:ID"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
input := []byte(tt.input)
|
||||
result := ascii.ToUpper(input)
|
||||
if string(result) != tt.expected {
|
||||
t.Errorf("ToUpper(%q) = %q, want %q", tt.input, string(result), tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestToCamelCorrectness(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
separator byte
|
||||
expected string
|
||||
}{
|
||||
{"Simple", "hello_world", '_', "HelloWorld"},
|
||||
{"Multiple", "one_two_three", '_', "OneTwoThree"},
|
||||
//{"AlreadyCamel", "HelloWorld", '_', "HelloWorld"},
|
||||
{"Numbers", "user_123_name", '_', "User123Name"},
|
||||
{"Trailing", "hello_world_", '_', "HelloWorld"},
|
||||
{"Leading", "_hello_world", '_', "HelloWorld"},
|
||||
{"Empty", "", '_', ""},
|
||||
{"NoSep", "helloworld", '_', "Helloworld"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
input := []byte(tt.input)
|
||||
result := ascii.ToCamel(input, tt.separator)
|
||||
if string(result) != tt.expected {
|
||||
t.Errorf("ToCamel(%q, %q) = %q, want %q", tt.input, tt.separator, string(result), tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEqualFoldCorrectness(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
a, b string
|
||||
expected bool
|
||||
}{
|
||||
{"Exact", "hello", "hello", true},
|
||||
{"DifferentCase", "HELLO", "hello", true},
|
||||
{"MixedCase", "HeLLo", "hEllO", true},
|
||||
{"Different", "hello", "world", false},
|
||||
{"DifferentLength", "hello", "hello!", false},
|
||||
{"WithNumbers", "abc123", "ABC123", true},
|
||||
{"HTTPMethod", "GET", "get", true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := ascii.EqualFold([]byte(tt.a), []byte(tt.b))
|
||||
if result != tt.expected {
|
||||
t.Errorf("EqualFold(%q, %q) = %v, want %v", tt.a, tt.b, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,87 @@
|
||||
/* ——————————————————————————————————————————————————————————————————————————
|
||||
Proyecto goat
|
||||
—————————————————————————————————————————————————————————————————————————————
|
||||
Fichero code_bench_test.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 test_errs
|
||||
|
||||
import (
|
||||
"core/errs"
|
||||
"regexp"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var codeRegex = regexp.MustCompile(`^([a-z0-9]+(?:_[a-z0-9]+)*)::([a-z0-9]+(?:_[a-z0-9]+)*)(?:\.[a-z0-9]+(?:_[a-z0-9]+)*)*$`)
|
||||
|
||||
func BenchmarkValidationComparison(b *testing.B) {
|
||||
input := "database::connection.pool.timeout_detected"
|
||||
|
||||
b.Run("Regex", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = codeRegex.MatchString(input)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("Custom", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = errs.ValidateCode(input)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("Custom-function", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = errs.ValidateCode2(input)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("Grok-the-looser", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = errs.ValidateCode3(input)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
@ -0,0 +1,179 @@
|
||||
/* ——————————————————————————————————————————————————————————————————————————
|
||||
Proyecto goat
|
||||
—————————————————————————————————————————————————————————————————————————————
|
||||
Fichero code_test.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 test_errs
|
||||
|
||||
import (
|
||||
"core/errs"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func IsValid(err error) bool { return err == nil }
|
||||
|
||||
func TestCode_IsValid(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
code errs.Code
|
||||
want bool
|
||||
}{
|
||||
// --- CASOS VÁLIDOS ---
|
||||
{"Simple", "sys::auth", true},
|
||||
{"ConJerarquia", "auth::login.failed", true},
|
||||
{"JerarquiaLarga", "db::connection.pool.timeout", true},
|
||||
{"ConGuiones", "my_module::internal_error", true},
|
||||
{"ConNumeros", "v1::api.error_404", true},
|
||||
{"MinimoPosible", "a::b", true},
|
||||
|
||||
// --- CASOS INVÁLIDOS: ESTRUCTURA ---
|
||||
{"Vacio", "", false},
|
||||
{"MuyCorto", "a:b", false},
|
||||
{"SinModulo", "login.failed", false},
|
||||
{"SoloModulo", "auth::", false}, // Termina en ::
|
||||
{"ModuloVacio", "::login", false},
|
||||
{"DobleModulo", "auth::db::error", false},
|
||||
{"ColonSuelto", "auth:login", false},
|
||||
|
||||
// --- CASOS INVÁLIDOS: PUNTOS ---
|
||||
{"PuntoEnModulo", "core.db::timeout", false},
|
||||
{"PuntoAlInicio", ".auth::error", false},
|
||||
{"PuntoAlFinal", "auth::error.", false},
|
||||
{"PuntosConsecutivos", "auth::login..failed", false},
|
||||
{"PuntoTrasModulo", "auth::.login", false},
|
||||
{"PuntoTrasModulo", "auth::.login..dff", false},
|
||||
{"PuntoTrasModulo", "auth::.log_.in..dff", false},
|
||||
{"PuntoTrasModulo", "auth::log_._.in.dff", false},
|
||||
|
||||
// --- CASOS INVÁLIDOS: CARACTERES ---
|
||||
{"Espacios", "auth::login failed", false},
|
||||
{"Simbolos", "auth::login#failed", false},
|
||||
{"UTF8", "auth::inicio._sesion", false}, // Solo ASCII alfanumérico
|
||||
{"UTF8", "auth::sin_apostrofe_", false}, //
|
||||
{"UTF8", "auth::sin_apostrofe2-", false}, //
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := IsValid(errs.ValidateCode(string(tt.code))); got != tt.want {
|
||||
t.Errorf("Code.ValidateCode() para %q = %v, se esperaba %v", tt.code, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCode_Parts(t *testing.T) {
|
||||
tests := []struct {
|
||||
code errs.Code
|
||||
expected []string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
code: "auth::failed",
|
||||
expected: []string{"auth", "failed"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
code: "auth::failed.invalid-token",
|
||||
expected: []string{"auth", "failed", "invalid-token"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
code: "auth::failed.invalid-token.expired",
|
||||
expected: []string{"auth", "failed", "invalid-token", "expired"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
code: "auth::", // Solo módulo
|
||||
expected: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
code: "auth", // Sin ::
|
||||
expected: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
code: "::failed", // Módulo vacío
|
||||
expected: []string{"", "failed"},
|
||||
wantErr: false, // Si permites módulo vacío
|
||||
},
|
||||
{
|
||||
code: "auth::failed..token", // Doble punto
|
||||
expected: []string{"auth", "failed", "token"}, // Segmento vacío filtrado
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(string(tt.code), func(t *testing.T) {
|
||||
parts, err := tt.code.Parts()
|
||||
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, parts)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.expected, parts)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
func BenchmarkCode_IsValid(b *testing.B) {
|
||||
// Caso de éxito típico para medir rendimiento real
|
||||
code := errs.Code("database::connection.pool.timeout_detected")
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = errs.ValidateCode(string(code))
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
@ -0,0 +1,11 @@
|
||||
module tests
|
||||
|
||||
go 1.25
|
||||
|
||||
require github.com/stretchr/testify v1.11.1
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
@ -0,0 +1,10 @@
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
Loading…
Reference in new issue