commit d0ef9f5eee0e76e3aaf3a5aa3bb8012c72519be7 Author: dev Date: Wed Feb 4 22:05:57 2026 +0100 first diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -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 diff --git a/.idea/goat.iml b/.idea/goat.iml new file mode 100644 index 0000000..5e764c4 --- /dev/null +++ b/.idea/goat.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..78bd62f --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..0b7a010 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module goat + +go 1.25 diff --git a/go.work b/go.work new file mode 100644 index 0000000..d634fd7 --- /dev/null +++ b/go.work @@ -0,0 +1,6 @@ +go 1.25 + +use ( + ./pkg/core + ./pkg/tests +) \ No newline at end of file diff --git a/go.work.sum b/go.work.sum new file mode 100644 index 0000000..93e5f73 --- /dev/null +++ b/go.work.sum @@ -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= diff --git a/pkg/core/btes/a.go b/pkg/core/btes/a.go new file mode 100644 index 0000000..b7cf772 --- /dev/null +++ b/pkg/core/btes/a.go @@ -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() +) diff --git a/pkg/core/btes/ascii_attrs.go b/pkg/core/btes/ascii_attrs.go new file mode 100644 index 0000000..bf1e482 --- /dev/null +++ b/pkg/core/btes/ascii_attrs.go @@ -0,0 +1,1403 @@ +/* —————————————————————————————————————————————————————————————————————————— + Proyecto goat +————————————————————————————————————————————————————————————————————————————— + Fichero ascii_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 proporciona operaciones de alto rendimiento sobre bytes ASCII +// mediante el uso de tablas de búsqueda (Lookup Tables - LUT) para clasificación +// y transformación en tiempo constante O(1). +// +// La estrategia principal es pre-calcular atributos de cada byte ASCII posible +// (256 valores) en una tabla de 256 entradas, permitiendo verificar propiedades +// de caracteres mediante simples operaciones bitwise sin comparaciones costosas. +// +// # Casos de Uso Principales +// +// - Routing HTTP: normalización de paths, métodos y headers +// - Parsing de URLs: detección de caracteres válidos, decode hexadecimal +// - Validación de identificadores: verificación alfanumérica rápida +// - Transformación de strings: case conversions sin allocations innecesarias +// +// # Ventajas de Rendimiento +// +// - IsAlpha(ch): ~1ns vs 8-15ns con unicode.IsLetter +// - ToUpper(s): 4x más rápido que implementación naive con allocation tardía +// - EqualFold(a,b): ~27ns vs ~38ns con strings.EqualFold para ASCII puro +package btes + +// ════════════════════════════════════════════════════════════════════════════ +// CONSTANTES DE ATRIBUTOS ASCII +// ════════════════════════════════════════════════════════════════════════════ + +// Máscaras de bits para clasificar caracteres ASCII. Cada constante representa +// un bit diferente, permitiendo que un byte tenga múltiples atributos simultáneos +// mediante operaciones OR (|). +// +// Ejemplo: 'A' tiene atributos: ASCIIAttrPrintable | ASCIIAttrAlpha | ASCIIAttrAlphaUpper +const ( + // ASCIIAttrControl identifica caracteres de control ASCII (0-31, 127). + // Incluye: \n, \r, \t, ESC, NULL, etc. + ASCIIAttrControl Attr = 1 << iota + + // ASCIIAttrPrintable identifica caracteres imprimibles (32-126). + // Excluye caracteres de control pero incluye espacio (32). + ASCIIAttrPrintable + + // ASCIIAttrAlpha identifica letras (A-Z, a-z). + // Útil para validar identificadores o paths alfabéticos. + ASCIIAttrAlpha + + // ASCIIAttrAlphaLower identifica letras minúsculas (a-z). + ASCIIAttrAlphaLower + + // ASCIIAttrAlphaUpper identifica letras mayúsculas (A-Z). + ASCIIAttrAlphaUpper + + // ASCIIAttrDigit identifica dígitos decimales ('0'-'9'). + ASCIIAttrDigit + + // ASCIIAttrHex identifica dígitos hexadecimales adicionales (A-F, a-f). + // No incluye 0-9, que ya tienen ASCIIAttrDigit. + ASCIIAttrHex + + // ASCIIAttrDigitHex es una máscara combinada para verificar si un byte + // es un dígito hexadecimal válido (0-9, A-F, a-f). + // Equivale a: ASCIIAttrHex | ASCIIAttrDigit + ASCIIAttrDigitHex = ASCIIAttrHex | ASCIIAttrDigit + + ASCIIAttrAlphaNum = ASCIIAttrAlpha | ASCIIAttrDigit + ASCIIAttrAlphaNumLower = ASCIIAttrAlphaLower | ASCIIAttrDigit + ASCIIAttrAlphaNumUpper = ASCIIAttrAlphaUpper | ASCIIAttrDigit +) + +// ════════════════════════════════════════════════════════════════════════════ +// TIPO PRINCIPAL: ASCIIAttrs +// ════════════════════════════════════════════════════════════════════════════ + +// ASCIIAttrs encapsula una tabla de atributos (LUT) para los 256 valores ASCII. +// Cada entrada de la tabla contiene los atributos del byte correspondiente +// codificados como bits en un uint8. +// +// La estructura es inmutable después de la inicialización, lo que permite +// compartirla de forma segura entre goroutines sin sincronización. +// +// # Complejidad de Operaciones +// +// - Verificación de atributos: O(1) - un lookup + AND bitwise +// - Transformaciones individuales: O(1) - un lookup + aritmética +// - Transformaciones de slices: O(n) - donde n es el tamaño del slice +// +// # Uso de Memoria +// +// - Tamaño de tabla: 256 bytes (una entrada por byte ASCII posible) +// - Overhead por instancia: ~8 bytes (puntero + metadatos struct) +// +// # Ejemplo Básico +// +// attrs := NewASCIIAttrs() +// +// // Verificaciones O(1) +// if attrs.IsDigit('5') { +// // true +// } +// +// // Transformaciones con zero-copy cuando sea posible +// upper := attrs.ToUpper([]byte("hello")) // "HELLO" +// same := attrs.ToUpper([]byte("HELLO")) // retorna input (zero-copy) +type ASCIIAttrs struct { + // attrs es la tabla interna de 256 entradas que mapea cada byte ASCII + // a sus atributos codificados como bits. + attrs Attrs +} + +// ════════════════════════════════════════════════════════════════════════════ +// CONSTRUCTOR +// ════════════════════════════════════════════════════════════════════════════ + +// NewASCIIAttrs crea y retorna una nueva instancia de ASCIIAttrs con la tabla +// de atributos ASCII completamente inicializada. +// +// La tabla se pre-calcula una sola vez en la construcción con los siguientes +// atributos para cada rango de caracteres ASCII: +// +// - Control (0-31, 127): Caracteres no imprimibles +// - Printable (32-126): Caracteres imprimibles estándar +// - Digit ('0'-'9'): Dígitos decimales +// - AlphaUpper ('A'-'Z'): Letras mayúsculas +// - AlphaLower ('a'-'z'): Letras minúsculas +// - Hex ('A'-'F', 'a'-'f'): Dígitos hexadecimales adicionales +// +// # Complejidad +// +// - Tiempo: O(256) - inicialización de tabla completa +// - Memoria: 256 bytes para la tabla + overhead de struct +// +// # Thread Safety +// +// Esta función es thread-safe. Cada llamada retorna una nueva instancia +// independiente que puede usarse concurrentemente sin sincronización. +// +// # Ejemplo +// +// // Típicamente se crea una instancia global +// var asciiAttrs = NewASCIIAttrs() +// +// // O se crea bajo demanda +// func processPath(path []byte) { +// attrs := NewASCIIAttrs() +// upper := attrs.ToUpper(path) +// // ... +// } +func NewASCIIAttrs() *ASCIIAttrs { + a := &ASCIIAttrs{} + + // Inicializar caracteres de control (0-31 + DEL) + a.attrs.SetRange(ASCIIAttrControl, 0, 31) + a.attrs[127] = ASCIIAttrControl + + // Inicializar caracteres imprimibles (espacio hasta ~) + a.attrs.SetRange(ASCIIAttrPrintable, 32, 126) + + // Inicializar dígitos decimales + a.attrs.AddRange(ASCIIAttrDigit, '0', '9') + + // Inicializar letras (marcando Alpha + Upper/Lower según corresponda) + a.attrs.AddRange(ASCIIAttrAlpha|ASCIIAttrAlphaUpper, 'A', 'Z') + a.attrs.AddRange(ASCIIAttrAlpha|ASCIIAttrAlphaLower, 'a', 'z') + + // Inicializar dígitos hexadecimales adicionales (A-F, a-f) + // Los dígitos 0-9 ya están marcados con ASCIIAttrDigit + a.attrs.AddRange(ASCIIAttrHex, 'A', 'F') + a.attrs.AddRange(ASCIIAttrHex, 'a', 'f') + + return a +} + +// ════════════════════════════════════════════════════════════════════════════ +// COMPARACIÓN CASE-INSENSITIVE +// ════════════════════════════════════════════════════════════════════════════ + +// EqualFold compara dos slices de bytes ignorando diferencias de mayúsculas/minúsculas +// en letras ASCII. Es equivalente a strings.EqualFold pero optimizado para ASCII puro. +// +// # Algoritmo +// +// 1. Verificación de longitud (early exit si difieren) +// 2. Comparación byte por byte: +// - Si son iguales → continuar +// - Si difieren → normalizar a minúsculas con |0x20 y verificar si son letras +// +// # Técnica de Normalización +// +// Usa el truco de |0x20 para convertir cualquier letra ASCII a minúscula: +// - 'A' (65) | 0x20 = 'a' (97) +// - 'a' (97) | 0x20 = 'a' (97) +// - '5' (53) | 0x20 = 'u' (117) ← Por eso verificamos IsAlpha después +// +// # Complejidad +// +// - Tiempo: O(n) donde n = len(source) +// - Memoria: O(1) - no allocations +// - Early exit: Retorna false inmediatamente al primer mismatch +// +// # Performance +// +// - ~27ns para strings típicos de 8-12 bytes (métodos HTTP, headers cortos) +// - ~38% más rápido que strings.EqualFold para ASCII puro +// - Beneficio aumenta con strings más largos por mejor inlining y cache locality +// +// # Casos de Uso +// +// - Comparar métodos HTTP: "GET" vs "get" +// - Comparar headers HTTP: "Content-Type" vs "content-type" +// - Routing case-insensitive: "/API/Users" vs "/api/users" +// +// # Ejemplo +// +// attrs := NewASCIIAttrs() +// +// // Comparaciones case-insensitive +// attrs.EqualFold([]byte("GET"), []byte("get")) // true +// attrs.EqualFold([]byte("Content-Type"), []byte("content-type")) // true +// attrs.EqualFold([]byte("hello"), []byte("world")) // false +// attrs.EqualFold([]byte("abc"), []byte("ABC123")) // false (longitud) +// +// # Limitaciones +// +// Solo funciona correctamente con ASCII. Para Unicode, usar strings.EqualFold +// o unicode.SimpleFold. +func (t *ASCIIAttrs) EqualFold(source, target []byte) bool { + // Early exit: longitudes diferentes nunca pueden ser iguales + if len(source) != len(target) { + return false + } + + // Comparación byte a byte + for i := 0; i < len(source); i++ { + s, tr := source[i], target[i] + + // Fast path: bytes exactamente iguales + if s == tr { + continue + } + + // Slow path: verificar si son la misma letra en diferentes casos + // |0x20 convierte A-Z a a-z (si es letra) + // Luego verificamos que realmente sea una letra para evitar falsos positivos + if (s|0x20) == (tr|0x20) && t.attrs[s]&ASCIIAttrAlpha != 0 { + continue + } + + // No son iguales ni case-insensitive + return false + } + + return true +} + +// SimpleLetterEqualFold verifica si dos slices tienen el mismo patrón de +// mayúsculas/minúsculas en las posiciones de letras. +// +// A diferencia de EqualFold, esta función NO ignora las diferencias de case. +// En su lugar, verifica que cuando hay una letra mayúscula en source[i], también +// hay una letra mayúscula en target[i] (aunque sean letras diferentes). +// +// # Casos de Uso +// +// - Validar consistencia de formato en identificadores +// - Verificar que el patrón de capitalización sea consistente +// - Validación de schemas donde el case pattern importa +// +// # Ejemplo +// +// attrs := NewASCIIAttrs() +// +// // Mismo patrón de case (ambos empiezan con mayúscula) +// attrs.SimpleLetterEqualFold([]byte("HelloWorld"), []byte("GreatThing")) // true +// +// // Diferente patrón (uno empieza minúscula) +// attrs.SimpleLetterEqualFold([]byte("hello"), []byte("World")) // false +// +// // Mismo contenido pero en ambos +// attrs.SimpleLetterEqualFold([]byte("Hello"), []byte("Hello")) // true +// +// # Complejidad +// +// - Tiempo: O(n) +// - Memoria: O(1) +func (t *ASCIIAttrs) SimpleLetterEqualFold(source, target []byte) bool { + if len(source) != len(target) { + return false + } + + for i, b := range source { + // Verificar que ambos bytes tengan el mismo atributo de case + // (ambos uppercase o ambos no-uppercase) + if t.IsAlphaUpper(b) != t.IsAlphaUpper(target[i]) { + return false + } + } + + return true +} + +// ════════════════════════════════════════════════════════════════════════════ +// CONVERSIONES DE CASE - BYTE INDIVIDUAL +// ════════════════════════════════════════════════════════════════════════════ + +// ToUpperByte convierte un byte individual a mayúscula si es una letra minúscula ASCII. +// +// # Algoritmo +// +// En ASCII, la diferencia entre una letra mayúscula y su equivalente minúscula es 32: +// - 'a' = 97, 'A' = 65 → diferencia = 32 +// - 'z' = 122, 'Z' = 90 → diferencia = 32 +// +// Por lo tanto: mayúscula = minúscula - 32 +// +// # Complejidad +// +// - Tiempo: O(1) - un lookup + una resta condicional +// - Memoria: O(1) - sin allocations +// +// # Ejemplo +// +// attrs := NewASCIIAttrs() +// attrs.ToUpperByte('a') // 'A' +// attrs.ToUpperByte('Z') // 'Z' (sin cambios) +// attrs.ToUpperByte('5') // '5' (sin cambios) +func (t *ASCIIAttrs) ToUpperByte(chr byte) byte { + if t.attrs[chr]&ASCIIAttrAlphaLower != 0 { + return chr - 32 + } + return chr +} + +// ToLowerByte convierte un byte individual a minúscula si es una letra mayúscula ASCII. +// +// # Algoritmo +// +// Inverso de ToUpperByte: minúscula = mayúscula + 32 +// +// # Complejidad +// +// - Tiempo: O(1) +// - Memoria: O(1) +// +// # Ejemplo +// +// attrs := NewASCIIAttrs() +// attrs.ToLowerByte('A') // 'a' +// attrs.ToLowerByte('z') // 'z' (sin cambios) +// attrs.ToLowerByte('5') // '5' (sin cambios) +func (t *ASCIIAttrs) ToLowerByte(chr byte) byte { + if t.attrs[chr]&ASCIIAttrAlphaUpper != 0 { + return chr + 32 + } + return chr +} + +// ════════════════════════════════════════════════════════════════════════════ +// CONVERSIONES DE CASE - IN-PLACE (MUTANTES) +// ════════════════════════════════════════════════════════════════════════════ + +// Upperize modifica el slice in-place convirtiendo todas las letras minúsculas +// a mayúsculas. Es la versión más rápida cuando se puede mutar el slice original. +// +// # Performance +// +// - ~43ns para slice típico de 16 bytes +// - Sin allocations (0 B/op) +// - Cache-friendly: acceso secuencial lineal +// +// # Casos de Uso +// +// - Normalizar paths temporales antes de lookup en tabla +// - Procesar buffers reutilizables +// - Cuando el slice original no se necesita preservar +// +// # Ejemplo +// +// attrs := NewASCIIAttrs() +// +// path := []byte("hello/world") +// attrs.Upperize(path) +// // path ahora es "HELLO/WORLD" +// +// // CUIDADO: El slice original está modificado +// original := []byte("test") +// attrs.Upperize(original) +// fmt.Println(string(original)) // "TEST" (¡modificado!) +// +// # Complejidad +// +// - Tiempo: O(n) donde n = len(list) +// - Memoria: O(1) - no allocations +func (t *ASCIIAttrs) Upperize(list []byte) { + for ix, ch := range list { + if t.attrs[ch]&ASCIIAttrAlphaLower != 0 { + list[ix] -= 32 + } + } +} + +// Lowerize modifica el slice in-place convirtiendo todas las letras mayúsculas +// a minúsculas. +// +// Equivalente a Upperize pero en dirección opuesta. Ver Upperize para más detalles. +// +// # Ejemplo +// +// attrs := NewASCIIAttrs() +// +// method := []byte("GET") +// attrs.Lowerize(method) +// // method ahora es "get" +func (t *ASCIIAttrs) Lowerize(list []byte) { + for ix, ch := range list { + if t.attrs[ch]&ASCIIAttrAlphaUpper != 0 { + list[ix] += 32 + } + } +} + +// ════════════════════════════════════════════════════════════════════════════ +// CONVERSIONES DE CASE - COPY-ON-WRITE (INMUTABLES) +// ════════════════════════════════════════════════════════════════════════════ + +// ToUpper devuelve una copia del slice con todas las letras minúsculas convertidas +// a mayúsculas. Si el slice ya está completamente en mayúsculas, retorna el slice +// original sin hacer copia (zero-copy optimization). +// +// # Algoritmo Optimizado (Pre-Scan Strategy) +// +// 1. Pre-scan: Escanear el slice completo buscando minúsculas +// - Si no encuentra ninguna → retornar slice original (zero-copy) +// - Si encuentra al menos una → proceder a paso 2 +// +// 2. Allocation: Crear nuevo slice del mismo tamaño UNA vez +// +// 3. Transform: Recorrer original y copiar/transformar a resultado +// - Si es minúscula → copiar como mayúscula (byte - 32) +// - Si no es minúscula → copiar tal cual +// +// # Por Qué Esta Estrategia es Óptima +// +// ## Comparación con Estrategia Naive: +// +// // ❌ NAIVE (malo): +// for ix, ch := range list { +// if isLower(ch) { +// if result == nil { +// result = make([]byte, len(list)) // Allocation tardía +// copy(result, list[:ix]) // Copy de bytes ya visitados +// } +// result[ix] = ch - 32 +// } +// } +// +// Problemas de estrategia naive: +// - Allocation DENTRO del loop (primera minúscula encontrada) +// - copy() ejecutado DESPUÉS de iterar ix bytes +// - En peor caso (minúscula al final): itera N → alloc → copy N → transforma 1 +// - Total: 2N operaciones de memoria +// +// ## Ventajas de Pre-Scan: +// +// - Máximo 2 scans completos (pre-scan + transform) +// - Allocation UNA vez al principio +// - No hay copy() separado (integrado en transform) +// - Zero-copy cuando no hay cambios (común en HTTP: métodos ya uppercase) +// +// # Performance +// +// - ~75ns para slice típico con cambios (4x mejora vs naive 307ns) +// - ~5ns para slice sin cambios (zero-copy, solo pre-scan) +// - 1 allocation cuando necesario vs allocations múltiples en naive +// +// # Casos de Uso +// +// - Normalizar métodos HTTP: "get" → "GET" (pero "GET" → "GET" zero-copy) +// - Normalizar headers HTTP antes de comparación +// - Convertir paths a formato canónico +// - Cualquier caso donde el original debe preservarse +// +// # Ejemplo +// +// attrs := NewASCIIAttrs() +// +// // Caso 1: Necesita transformación (hace copia) +// lower := []byte("hello") +// upper := attrs.ToUpper(lower) // "HELLO" (nuevo slice) +// // lower sigue siendo "hello" (original preservado) +// +// // Caso 2: Ya está uppercase (zero-copy) +// already := []byte("HELLO") +// same := attrs.ToUpper(already) // retorna already (mismo slice) +// // No hay allocation ni copia +// +// // Caso 3: Mezclado (hace copia) +// mixed := []byte("HeLLo") +// result := attrs.ToUpper(mixed) // "HELLO" (nuevo slice) +// +// # Complejidad +// +// - Tiempo: +// - Mejor caso (sin cambios): O(n) - solo pre-scan +// - Peor caso (con cambios): O(2n) - pre-scan + transform +// - Memoria: +// - Mejor caso: O(1) - no allocations +// - Peor caso: O(n) - un nuevo slice +// +// # Thread Safety +// +// Es thread-safe para el slice de entrada (no lo modifica). Cada llamada que +// necesita transformación retorna un nuevo slice independiente. +func (t *ASCIIAttrs) ToUpper(list []byte) []byte { + // FASE 1: Pre-scan para detectar si hay minúsculas + // Early exit si no hay cambios necesarios + needsChange := false + for _, ch := range list { + if t.attrs[ch]&ASCIIAttrAlphaLower != 0 { + needsChange = true + break // No necesitamos seguir escaneando + } + } + + // Zero-copy optimization: retornar original si ya está uppercase + if !needsChange { + return list + } + + // FASE 2: Allocation UNA vez + transformation en single-pass + result := make([]byte, len(list)) + for i, ch := range list { + if t.attrs[ch]&ASCIIAttrAlphaLower != 0 { + result[i] = ch - 32 // Convertir a mayúscula + } else { + result[i] = ch // Copiar tal cual + } + } + + return result +} + +// ToLower devuelve una copia del slice con todas las letras mayúsculas convertidas +// a minúsculas. Usa la misma estrategia optimizada que ToUpper. +// +// Ver documentación de ToUpper para detalles del algoritmo y optimizaciones. +// +// # Ejemplo +// +// attrs := NewASCIIAttrs() +// +// upper := []byte("HELLO") +// lower := attrs.ToLower(upper) // "hello" (nuevo slice) +// +// alreadyLower := []byte("hello") +// same := attrs.ToLower(alreadyLower) // retorna alreadyLower (zero-copy) +func (t *ASCIIAttrs) ToLower(list []byte) []byte { + // Pre-scan para detectar mayúsculas + needsChange := false + for _, ch := range list { + if t.attrs[ch]&ASCIIAttrAlphaUpper != 0 { + needsChange = true + break + } + } + + // Zero-copy si no hay cambios + if !needsChange { + return list + } + + // Single allocation + transform + result := make([]byte, len(list)) + for i, ch := range list { + if t.attrs[ch]&ASCIIAttrAlphaUpper != 0 { + result[i] = ch + 32 // Convertir a minúscula + } else { + result[i] = ch + } + } + + return result +} + +// ════════════════════════════════════════════════════════════════════════════ +// TRANSFORMACIONES DE NAMING CONVENTIONS +// ════════════════════════════════════════════════════════════════════════════ + +// Camelize transforma el slice a CamelCase in-place, eliminando los separadores +// y capitalizando la primera letra de cada palabra. +// +// # Formato CamelCase +// +// En CamelCase, cada palabra (excepto posiblemente la primera) comienza con mayúscula +// y no hay espacios ni separadores: +// - snake_case "hello_world" → CamelCase "HelloWorld" +// - kebab-case "hello-world" → CamelCase "HelloWorld" +// +// # Comportamiento +// +// - Elimina todos los separadores encontrados +// - Capitaliza la primera letra después de cada separador (o al inicio) +// - Normaliza el resto de letras a minúsculas +// - El slice se compacta (reduce tamaño) si había separadores +// +// # IMPORTANTE: Modificación In-Place +// +// Esta función modifica el slice original Y retorna un re-slice con la nueva +// longitud (menor si había separadores). El slice retornado comparte el mismo +// backing array que el original. +// +// # Algoritmo +// +// 1. Usar dos índices: readIdx (lectura) y writeIdx (escritura) +// 2. Para cada byte leído: +// - Si es separador: marcar que siguiente letra debe capitalizarse, no escribir +// - Si debe capitalizarse: escribir como mayúscula, desmarcar flag +// - Sino: escribir como minúscula +// 3. Retornar slice[:writeIdx] con nueva longitud +// +// # Ejemplo +// +// attrs := NewASCIIAttrs() +// +// input := []byte("hello_world_test") +// result := attrs.Camelize(input, '_') +// // result: "HelloWorldTest" (len=14) +// // input: "HelloWorldTestst" (¡modificado! últimos bytes son basura) +// +// // SIEMPRE usar el slice retornado: +// camel := attrs.Camelize([]byte("user_service_handler"), '_') +// fmt.Println(string(camel)) // "UserServiceHandler" +// +// # Complejidad +// +// - Tiempo: O(n) +// - Memoria: O(1) - no allocations, modifica in-place +// +// # Casos de Uso +// +// - Transformar nombres de variables de snake_case a CamelCase +// - Normalizar identificadores desde diferentes convenciones +// - Procesar parámetros de configuración +func (t *ASCIIAttrs) Camelize(list []byte, separator byte) []byte { + if len(list) == 0 { + return list + } + + writeIdx := 0 + capitalizeNext := true + + for readIdx := 0; readIdx < len(list); readIdx++ { + ch := list[readIdx] + + // Si encontramos separador: marcamos que siguiente letra va en mayúscula + // y NO escribimos el separador (lo eliminamos) + if ch == separator { + capitalizeNext = true + continue // Salta el separador (compactación) + } + + // Aplicar transformación según estado + if capitalizeNext { + // Primera letra de palabra: capitalizar si es minúscula + if t.attrs[ch]&ASCIIAttrAlphaLower != 0 { + list[writeIdx] = ch - 32 // A mayúscula + } else { + list[writeIdx] = ch + } + // Solo desactivar capitalización si encontramos un alfanumérico + // (ignora símbolos/espacios al inicio de palabra) + if t.attrs[ch]&(ASCIIAttrAlpha|ASCIIAttrDigit) != 0 { + capitalizeNext = false + } + } else { + // Resto de la palabra: normalizar a minúsculas + if t.attrs[ch]&ASCIIAttrAlphaUpper != 0 { + list[writeIdx] = ch + 32 // A minúscula + } else { + list[writeIdx] = ch + } + } + writeIdx++ + } + + // Retornar re-slice con nueva longitud + return list[:writeIdx] +} + +// ToCamel convierte a CamelCase retornando un nuevo slice, preservando el original. +// Usa estrategia optimizada de pre-scan para evitar allocations innecesarias. +// +// # Diferencia con Camelize +// +// - Camelize: Modifica in-place, más rápido, usa mismo backing array +// - ToCamel: Crea copia si necesario, preserva original, zero-copy cuando posible +// +// # Algoritmo Optimizado +// +// 1. Pre-scan Phase: +// - Contar separadores (para calcular tamaño final) +// - Detectar si necesita cambios (early exit si ya está en formato correcto) +// +// 2. Decision: +// - Si no necesita cambios → retornar original (zero-copy) +// - Si necesita cambios → continuar a fase 3 +// +// 3. Transform Phase: +// - Allocar slice con tamaño final conocido (len - sepCount) +// - Copiar y transformar simultáneamente +// +// # Performance +// +// - ~115ns con transformación (4x mejora vs implementación naive) +// - ~10ns sin transformación (zero-copy) +// - Evita allocations innecesarias cuando input ya está en CamelCase +// +// # Ejemplo +// +// attrs := NewASCIIAttrs() +// +// // Transformación necesaria +// snake := []byte("hello_world") +// camel := attrs.ToCamel(snake, '_') +// // snake: "hello_world" (sin cambios) +// // camel: "HelloWorld" (nuevo slice) +// +// // Zero-copy (ya es CamelCase sin separadores) +// already := []byte("HelloWorld") +// same := attrs.ToCamel(already, '_') +// // same apunta a already (no hay copia) +// +// // Con múltiples separadores +// multi := []byte("user_service_handler") +// result := attrs.ToCamel(multi, '_') +// // result: "UserServiceHandler" +// +// # Complejidad +// +// - Tiempo: +// - Mejor caso (sin cambios): O(n) - solo pre-scan +// - Peor caso (con cambios): O(2n) - pre-scan + transform +// - Memoria: +// - Mejor caso: O(1) - zero-copy +// - Peor caso: O(n-s) - donde s = número de separadores +func (t *ASCIIAttrs) ToCamel(list []byte, separator byte) []byte { + if len(list) == 0 { + return list + } + + // FASE 1: Pre-scan para detectar cambios necesarios Y contar separadores + sepCount := 0 + needsChange := false + capitalizeNext := true + + for _, ch := range list { + if ch == separator { + sepCount++ + needsChange = true // Siempre necesita cambios si hay separadores + capitalizeNext = true + continue + } + + // Verificar si necesita cambio según posición + if capitalizeNext { + // Primera letra de palabra: debe ser mayúscula + if t.attrs[ch]&ASCIIAttrAlphaLower != 0 { + needsChange = true // Es minúscula, necesita cambio + } + // Consumir estado solo si es alfanumérico + if t.attrs[ch]&(ASCIIAttrAlpha|ASCIIAttrDigit) != 0 { + capitalizeNext = false + } + } else { + // Resto de la palabra: debe ser minúscula + if t.attrs[ch]&ASCIIAttrAlphaUpper != 0 { + needsChange = true // Es mayúscula, necesita normalización + } + } + } + + // Zero-copy optimization + if !needsChange { + return list + } + + // FASE 2: Allocation con tamaño final conocido + finalLen := len(list) - sepCount + if finalLen == 0 { + return []byte{} + } + + result := make([]byte, finalLen) + + // FASE 3: Transform en single-pass + i := 0 + capitalizeNext = true + + for _, ch := range list { + if ch == separator { + capitalizeNext = true + continue + } + + if capitalizeNext { + if t.attrs[ch]&ASCIIAttrAlphaLower != 0 { + result[i] = ch - 32 // A mayúscula + } else { + result[i] = ch + } + if t.attrs[ch]&(ASCIIAttrAlpha|ASCIIAttrDigit) != 0 { + capitalizeNext = false + } + } else { + if t.attrs[ch]&ASCIIAttrAlphaUpper != 0 { + result[i] = ch + 32 // A minúscula (normalización) + } else { + result[i] = ch + } + } + i++ + } + + return result +} + +// Snakeize convierte a snake_case in-place insertando separadores antes de mayúsculas. +// +// # ADVERTENCIA: Expansión de Tamaño +// +// A diferencia de Camelize (que compacta), Snakeize puede AUMENTAR el tamaño del slice +// al insertar separadores. Por esto, normalmente requiere un buffer de destino con +// espacio extra. +// +// Esta implementación calcula el espacio extra necesario y crea un nuevo slice si +// es necesario. Por lo tanto, NO es realmente in-place puro. +// +// # Algoritmo +// +// 1. Calcular espacio extra necesario contando mayúsculas +// 2. Si no hay mayúsculas → convertir a lowercase in-place y retornar +// 3. Si hay mayúsculas → crear nuevo slice con espacio extra y transformar +// +// # Ejemplo +// +// attrs := NewASCIIAttrs() +// +// camel := []byte("HelloWorld") +// snake := attrs.Snakeize(camel, '_') +// // snake: "hello_world" +// +// mixed := []byte("getUserByID") +// result := attrs.Snakeize(mixed, '_') +// // result: "get_user_by_i_d" +// +// # Complejidad +// +// - Tiempo: O(n) +// - Memoria: O(n+e) donde e = número de mayúsculas a insertar +func (t *ASCIIAttrs) Snakeize(list []byte, separator byte) []byte { + // Calcular espacio extra necesario + extra := 0 + for i, ch := range list { + if i > 0 && t.attrs[ch]&ASCIIAttrAlphaUpper != 0 { + extra++ + } + } + + // Si no hay mayúsculas, solo convertir a lowercase + if extra == 0 { + t.Lowerize(list) + return list + } + + // Crear nuevo slice con espacio para separadores + newLen := len(list) + extra + result := make([]byte, newLen) + target := 0 + + for i := 0; i < len(list); i++ { + ch := list[i] + + // Insertar separador antes de mayúscula (excepto primera posición) + if i > 0 && t.attrs[ch]&ASCIIAttrAlphaUpper != 0 { + result[target] = separator + target++ + result[target] = ch + 32 // Convertir a lowercase + } else if t.attrs[ch]&ASCIIAttrAlphaUpper != 0 { + result[target] = ch + 32 // Primera letra: solo lowercase + } else { + result[target] = ch + } + target++ + } + + return result +} + +// ToSnake convierte a snake_case manejando inteligentemente CamelCase y separadores. +// +// # Comportamiento Avanzado +// +// Esta función maneja varios casos complejos: +// +// 1. CamelCase → snake_case: +// - "HelloWorld" → "hello_world" +// - Inserta '_' antes de mayúscula si la anterior era minúscula/dígito +// +// 2. Siglas (secuencias de mayúsculas): +// - "HTMLParser" → "html_parser" (no "h_t_m_l_parser") +// - Detecta cuando una mayúscula es seguida por minúscula +// +// 3. Normalización de separadores: +// - Convierte '-', '.', ' ' al separador solicitado +// - "hello-world" → "hello_world" (si separator='_') +// - "hello.world" → "hello_world" +// +// # Algoritmo de Inserción Inteligente +// +// Inserta '_' antes de una mayúscula si: +// - La anterior era minúscula/dígito: "aB" → "a_b" +// - O forma parte de sigla antes de minúscula: "ABc" → "a_bc" +// +// # Ejemplo +// +// attrs := NewASCIIAttrs() +// +// // CamelCase básico +// attrs.ToSnake([]byte("HelloWorld"), '_') // "hello_world" +// +// // Siglas +// attrs.ToSnake([]byte("HTMLParser"), '_') // "html_parser" +// attrs.ToSnake([]byte("parseHTMLDocument"), '_') // "parse_html_document" +// +// // Normalización de separadores +// attrs.ToSnake([]byte("hello-world"), '_') // "hello_world" +// attrs.ToSnake([]byte("user.service"), '_') // "user_service" +// +// // Mix complejo +// attrs.ToSnake([]byte("getUserByID"), '_') // "get_user_by_id" +// +// # Performance +// +// - Pre-calcula tamaño final (evita reallocations) +// - Zero-copy cuando no hay cambios +// - Single-pass transformation +// +// # Complejidad +// +// - Tiempo: O(2n) - pre-scan + transform +// - Memoria: O(n+s) donde s = separadores insertados +func (t *ASCIIAttrs) ToSnake(list []byte, separator byte) []byte { + if len(list) == 0 { + return list + } + + // PASO 1: Calcular longitud extra y detectar necesidad de normalización + extraLen := 0 + needsNormalization := false + hasUpper := false + + for i := 0; i < len(list); i++ { + ch := list[i] + + if t.attrs[ch]&ASCIIAttrAlphaUpper != 0 { + hasUpper = true + + // Caso 1: "aB" → Insertar '_' entre minúscula/dígito y mayúscula + if i > 0 && (t.attrs[list[i-1]]&(ASCIIAttrAlphaLower|ASCIIAttrDigit) != 0) { + extraLen++ + } + + // Caso 2: "ABc" → Insertar '_' en "A_Bc" (manejo de siglas) + if i > 0 && i < len(list)-1 && + (t.attrs[list[i-1]]&ASCIIAttrAlphaUpper != 0) && + (t.attrs[list[i+1]]&ASCIIAttrAlphaLower != 0) { + extraLen++ + } + } else { + // Detectar separadores que necesitan normalización + if (ch == '-' || ch == '.' || ch == ' ' || ch == '_') && ch != separator { + needsNormalization = true + } + } + } + + // Zero-copy optimization + if extraLen == 0 && !hasUpper && !needsNormalization { + return list + } + + // PASO 2: Construcción del resultado + result := make([]byte, len(list)+extraLen) + w := 0 + + for i := 0; i < len(list); i++ { + ch := list[i] + + if t.attrs[ch]&ASCIIAttrAlphaUpper != 0 { + // Lógica de inserción de separador + if i > 0 && (t.attrs[list[i-1]]&(ASCIIAttrAlphaLower|ASCIIAttrDigit) != 0) { + result[w] = separator + w++ + } else if i > 0 && i < len(list)-1 && + (t.attrs[list[i-1]]&ASCIIAttrAlphaUpper != 0) && + (t.attrs[list[i+1]]&ASCIIAttrAlphaLower != 0) { + result[w] = separator + w++ + } + result[w] = ch + 32 // A minúscula + } else { + // Normalización de separadores + if ch == '-' || ch == '.' || ch == ' ' || ch == '_' { + result[w] = separator + } else { + result[w] = ch + } + } + w++ + } + + return result +} + +// ScreamingSnakeize transforma a SCREAMING_SNAKE_CASE in-place (limitado al tamaño actual). +// +// # SCREAMING_SNAKE_CASE +// +// Es snake_case pero con todas las letras en mayúsculas: +// - "hello_world" → "HELLO_WORLD" +// - Comúnmente usado para constantes: MAX_SIZE, API_KEY +// +// # Limitación +// +// No puede insertar separadores si no hay espacio (in-place real). Para una versión +// que inserta separadores, usar ToScreamingSnake. +// +// # Ejemplo +// +// attrs := NewASCIIAttrs() +// +// input := []byte("hello_world") +// attrs.ScreamingSnakeize(input) +// // input: "HELLO_WORLD" +// +// // Normaliza separadores conocidos +// input2 := []byte("hello-world") +// attrs.ScreamingSnakeize(input2) +// // input2: "HELLO_WORLD" +func (t *ASCIIAttrs) ScreamingSnakeize(list []byte) []byte { + for i := 0; i < len(list); i++ { + ch := list[i] + + // Convertir minúsculas a mayúsculas + if t.attrs[ch]&ASCIIAttrAlphaLower != 0 { + list[i] = ch - 32 + } else if ch == '-' || ch == '.' || ch == ' ' { + // Normalizar separadores conocidos a '_' + list[i] = '_' + } + } + return list +} + +// ToScreamingSnake convierte a SCREAMING_SNAKE_CASE con manejo completo de CamelCase. +// +// Combina la funcionalidad de ToSnake y uppercase: +// - Detecta CamelCase e inserta '_' +// - Maneja siglas inteligentemente +// - Normaliza separadores existentes +// - Convierte todo a mayúsculas +// +// # Ejemplo +// +// attrs := NewASCIIAttrs() +// +// // CamelCase → SCREAMING_SNAKE +// attrs.ToScreamingSnake([]byte("HelloWorld"), '_') // "HELLO_WORLD" +// attrs.ToScreamingSnake([]byte("getUserByID"), '_') // "GET_USER_BY_ID" +// +// // Normalización +// attrs.ToScreamingSnake([]byte("hello-world"), '_') // "HELLO_WORLD" +// attrs.ToScreamingSnake([]byte("api_key"), '_') // "API_KEY" +// +// # Complejidad +// +// - Tiempo: O(2n) +// - Memoria: O(n+s) +func (t *ASCIIAttrs) ToScreamingSnake(list []byte, separator byte) []byte { + if len(list) == 0 { + return list + } + + // Detectar necesidad de cambios + extraLen := 0 + needsUpper := false + needsNormalization := false + + for i := 0; i < len(list); i++ { + ch := list[i] + + // Detectar inserción de separadores + if i > 0 && t.attrs[ch]&ASCIIAttrAlphaUpper != 0 { + if t.attrs[list[i-1]]&(ASCIIAttrAlphaLower|ASCIIAttrDigit) != 0 { + extraLen++ + } else if i < len(list)-1 && t.attrs[list[i+1]]&ASCIIAttrAlphaLower != 0 { + extraLen++ + } + } + + // Detectar necesidad de uppercase + if t.attrs[ch]&ASCIIAttrAlphaLower != 0 { + needsUpper = true + } + + // Detectar necesidad de normalización + if (ch == '-' || ch == '.' || ch == ' ') && ch != separator { + needsNormalization = true + } + } + + // Zero-copy optimization + if extraLen == 0 && !needsUpper && !needsNormalization { + return list + } + + // Construcción + result := make([]byte, len(list)+extraLen) + w := 0 + + for i := 0; i < len(list); i++ { + ch := list[i] + + if t.attrs[ch]&ASCIIAttrAlphaUpper != 0 { + // Insertar separador si es necesario + if i > 0 && (t.attrs[list[i-1]]&(ASCIIAttrAlphaLower|ASCIIAttrDigit) != 0) { + result[w] = separator + w++ + } else if i > 0 && i < len(list)-1 && + (t.attrs[list[i-1]]&ASCIIAttrAlphaUpper != 0) && + (t.attrs[list[i+1]]&ASCIIAttrAlphaLower != 0) { + result[w] = separator + w++ + } + result[w] = ch // Ya es mayúscula + } else if t.attrs[ch]&ASCIIAttrAlphaLower != 0 { + result[w] = ch - 32 // Convertir a mayúscula + } else if ch == '-' || ch == '.' || ch == ' ' || ch == '_' { + result[w] = separator // Normalizar separador + } else { + result[w] = ch + } + w++ + } + + return result +} + +// ════════════════════════════════════════════════════════════════════════════ +// CONVERSIONES NUMÉRICAS +// ════════════════════════════════════════════════════════════════════════════ + +// DigitToByte convierte un byte dígito decimal ('0'-'9') a su valor numérico (0-9). +// +// # Ejemplo +// +// attrs := NewASCIIAttrs() +// attrs.DigitToByte('0') // 0 +// attrs.DigitToByte('5') // 5 +// attrs.DigitToByte('9') // 9 +// attrs.DigitToByte('A') // 0 (no es dígito) +// +// # Complejidad: O(1) +func (t *ASCIIAttrs) DigitToByte(chr byte) byte { + if t.attrs[chr]&ASCIIAttrDigit != 0 { + return chr - '0' + } + return 0 +} + +// HexDigitToByte convierte un byte hexadecimal ('0'-'9','A'-'F','a'-'f') a su valor (0-15). +// +// # Ejemplo +// +// attrs := NewASCIIAttrs() +// attrs.HexDigitToByte('0') // 0 +// attrs.HexDigitToByte('9') // 9 +// attrs.HexDigitToByte('A') // 10 +// attrs.HexDigitToByte('F') // 15 +// attrs.HexDigitToByte('a') // 10 +// attrs.HexDigitToByte('f') // 15 +// attrs.HexDigitToByte('G') // 0 (no es hex) +// +// # Complejidad: O(1) +func (t *ASCIIAttrs) HexDigitToByte(chr byte) byte { + att := t.attrs[chr] + if att&ASCIIAttrDigitHex != 0 { + if att&ASCIIAttrDigit != 0 { + return chr - '0' + } + if att&ASCIIAttrAlphaLower != 0 { + return chr - 'a' + 10 + } + return chr - 'A' + 10 + } + return 0 +} + +// IsHex2Digit verifica si dos bytes consecutivos son dígitos hexadecimales válidos. +// +// # Uso Típico +// +// Verificar antes de decodificar URL encoding: "%20" → ' ' +// +// # Ejemplo +// +// attrs := NewASCIIAttrs() +// attrs.IsHex2Digit('2', '0') // true ("%20") +// attrs.IsHex2Digit('F', 'F') // true ("%FF") +// attrs.IsHex2Digit('G', '0') // false (G no es hex) +// +// # Complejidad: O(1) +func (t *ASCIIAttrs) IsHex2Digit(cha, chb byte) bool { + return (t.attrs[cha]&ASCIIAttrDigitHex != 0) && (t.attrs[chb]&ASCIIAttrDigitHex != 0) +} + +// Hex2DigitToByte combina dos dígitos hexadecimales en un byte. +// +// # IMPORTANTE +// +// No valida si los bytes son hex válidos. Usar IsHex2Digit primero si no estás seguro. +// +// # Ejemplo +// +// attrs := NewASCIIAttrs() +// attrs.Hex2DigitToByte('2', '0') // 32 (espacio ' ') +// attrs.Hex2DigitToByte('F', 'F') // 255 +// attrs.Hex2DigitToByte('4', '1') // 65 ('A') +// +// # Uso en URL Decoding +// +// if attrs.IsHex2Digit(url[i+1], url[i+2]) { +// decoded = attrs.Hex2DigitToByte(url[i+1], url[i+2]) +// } +// +// # Complejidad: O(1) +func (t *ASCIIAttrs) Hex2DigitToByte(cha, chb byte) byte { + return t.HexDigitToByte(cha)<<4 | t.HexDigitToByte(chb) +} + +// GetHex2DigitToByte combina dos dígitos hex en un byte Y valida. +// +// A diferencia de Hex2DigitToByte, esta función valida y retorna un bool indicando éxito. +// +// # Ejemplo +// +// attrs := NewASCIIAttrs() +// +// val, ok := attrs.GetHex2DigitToByte('2', '0') +// if ok { +// // val = 32 (espacio) +// } +// +// val, ok = attrs.GetHex2DigitToByte('G', '0') +// // ok = false, val = 0 +// +// # Complejidad: O(1) +func (t *ASCIIAttrs) GetHex2DigitToByte(cha, chb byte) (byte, bool) { + at, bt := t.attrs[cha], t.attrs[chb] + + // Validar que ambos sean hex + if at&ASCIIAttrDigitHex == 0 || bt&ASCIIAttrDigitHex == 0 { + return 0, false + } + + // Convertir primer dígito + var va byte + if at&ASCIIAttrDigit != 0 { + va = cha - '0' + } else if at&ASCIIAttrAlphaLower != 0 { + va = cha - 'a' + 10 + } else { + va = cha - 'A' + 10 + } + va <<= 4 + + // Convertir segundo dígito + if bt&ASCIIAttrDigit != 0 { + return va | (chb - '0'), true + } + if bt&ASCIIAttrAlphaLower != 0 { + return va | (chb - 'a' + 10), true + } + return va | (chb - 'A' + 10), true +} + +// ════════════════════════════════════════════════════════════════════════════ +// PREDICADOS DE CLASIFICACIÓN +// ════════════════════════════════════════════════════════════════════════════ + +// Todas estas funciones son O(1) - un lookup + AND bitwise. + +// IsControl verifica si el byte es un carácter de control ASCII (0-31 o 127). +func (t *ASCIIAttrs) IsControl(a byte) bool { + return t.attrs[a]&ASCIIAttrControl != 0 +} + +// IsPrintable verifica si el byte es un carácter imprimible (32-126). +func (t *ASCIIAttrs) IsPrintable(a byte) bool { + return t.attrs[a]&ASCIIAttrPrintable != 0 +} + +// IsAlpha verifica si el byte es una letra (A-Z o a-z). +func (t *ASCIIAttrs) IsAlpha(a byte) bool { + return t.attrs[a]&ASCIIAttrAlpha != 0 +} + +// IsAlphaLower verifica si el byte es una letra minúscula (a-z). +func (t *ASCIIAttrs) IsAlphaLower(a byte) bool { + return t.attrs[a]&ASCIIAttrAlphaLower != 0 +} + +// IsAlphaUpper verifica si el byte es una letra mayúscula (A-Z). +func (t *ASCIIAttrs) IsAlphaUpper(a byte) bool { + return t.attrs[a]&ASCIIAttrAlphaUpper != 0 +} + +// IsDigit verifica si el byte es un dígito decimal ('0'-'9'). +func (t *ASCIIAttrs) IsDigit(a byte) bool { + return t.attrs[a]&ASCIIAttrDigit != 0 +} + +// IsHex verifica si el byte es un dígito hexadecimal adicional (A-F, a-f). +// No incluye 0-9. Para verificar cualquier dígito hex, usar IsHexDigit. +func (t *ASCIIAttrs) IsHex(a byte) bool { + return t.attrs[a]&ASCIIAttrHex != 0 +} + +// IsAlphaNum verifica si el byte es alfanumérico (letra o dígito). +func (t *ASCIIAttrs) IsAlphaNum(a byte) bool { + return t.attrs[a]&(ASCIIAttrAlphaNum) != 0 +} + +// IsAlphaNumLower verifica si el byte es alfanumérico (letra minúscula o dígito). +func (t *ASCIIAttrs) IsAlphaNumLower(a byte) bool { + return t.attrs[a]&(ASCIIAttrAlphaNumLower) != 0 +} + +// IsAlphaNumUpper verifica si el byte es alfanumérico (letra mayúscula o dígito). +func (t *ASCIIAttrs) IsAlphaNumUpper(a byte) bool { + return t.attrs[a]&(ASCIIAttrAlphaNumUpper) != 0 +} + +// IsHexDigit verifica si el byte es cualquier dígito hexadecimal (0-9, A-F, a-f). +func (t *ASCIIAttrs) IsHexDigit(a byte) bool { + return t.attrs[a]&ASCIIAttrDigitHex != 0 +} diff --git a/pkg/core/btes/attr.go b/pkg/core/btes/attr.go new file mode 100644 index 0000000..bb52267 --- /dev/null +++ b/pkg/core/btes/attr.go @@ -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 } diff --git a/pkg/core/btes/attrs.go b/pkg/core/btes/attrs.go new file mode 100644 index 0000000..cf89beb --- /dev/null +++ b/pkg/core/btes/attrs.go @@ -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 +} diff --git a/pkg/core/errs/a.go b/pkg/core/errs/a.go new file mode 100644 index 0000000..83ef3fb --- /dev/null +++ b/pkg/core/errs/a.go @@ -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 +) diff --git a/pkg/core/errs/all.gos b/pkg/core/errs/all.gos new file mode 100644 index 0000000..8b5b18b --- /dev/null +++ b/pkg/core/errs/all.gos @@ -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 +} diff --git a/pkg/core/errs/code.go b/pkg/core/errs/code.go new file mode 100644 index 0000000..badbc41 --- /dev/null +++ b/pkg/core/errs/code.go @@ -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 +} diff --git a/pkg/core/errs/module.go b/pkg/core/errs/module.go new file mode 100644 index 0000000..06a0665 --- /dev/null +++ b/pkg/core/errs/module.go @@ -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 +} diff --git a/pkg/core/go.mod b/pkg/core/go.mod new file mode 100644 index 0000000..03e6bec --- /dev/null +++ b/pkg/core/go.mod @@ -0,0 +1,3 @@ +module core + +go 1.25 diff --git a/pkg/tests/core/btes/ascii_bench_test.go b/pkg/tests/core/btes/ascii_bench_test.go new file mode 100644 index 0000000..90b96ea --- /dev/null +++ b/pkg/tests/core/btes/ascii_bench_test.go @@ -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, '_') + } + }) +} diff --git a/pkg/tests/core/btes/ascii_test.go b/pkg/tests/core/btes/ascii_test.go new file mode 100644 index 0000000..ef2059c --- /dev/null +++ b/pkg/tests/core/btes/ascii_test.go @@ -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) + } + }) + } +} diff --git a/pkg/tests/core/errs/code_bench_test.go b/pkg/tests/core/errs/code_bench_test.go new file mode 100644 index 0000000..34302db --- /dev/null +++ b/pkg/tests/core/errs/code_bench_test.go @@ -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) + } + }) + +} diff --git a/pkg/tests/core/errs/code_test.go b/pkg/tests/core/errs/code_test.go new file mode 100644 index 0000000..0ff7710 --- /dev/null +++ b/pkg/tests/core/errs/code_test.go @@ -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)) + } +} + +*/ diff --git a/pkg/tests/go.mod b/pkg/tests/go.mod new file mode 100644 index 0000000..e44e049 --- /dev/null +++ b/pkg/tests/go.mod @@ -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 +) diff --git a/pkg/tests/go.sum b/pkg/tests/go.sum new file mode 100644 index 0000000..c4c1710 --- /dev/null +++ b/pkg/tests/go.sum @@ -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=