diff --git a/src/apps/apperrs/main.go b/src/apps/apperrs/main.go index 494c31c..a4c3c18 100644 --- a/src/apps/apperrs/main.go +++ b/src/apps/apperrs/main.go @@ -72,7 +72,7 @@ func main() { errs.Code("::error_cause"),"Esto es un mensaje %d ",3333), errs.WithStack(errs.Code("POr otro error"),10))))) - errs.NewTree(otro,errs.WithMaxStackDepth(-1)).Print(os.Stdout) + errs.CTreeWriter(errs.NewTree(otro,errs.WithMaxStackDepth(-1)),os.Stdout) diff --git a/src/core/errs/a.go b/src/core/errs/a.go index 64c8944..3cf5193 100644 --- a/src/core/errs/a.go +++ b/src/core/errs/a.go @@ -53,6 +53,7 @@ import ( "errors" "fmt" "regexp" + "sync" ) const ( @@ -66,11 +67,14 @@ const ( var ( + rw = sync.RWMutex{} paramRgx = regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9_]{0,62}[a-zA-Z0-9]$`) metaRgx = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_-]{0,62}[a-zA-Z0-9_]$`) codeRgx = regexp.MustCompile(`^[a-zA-Z0-9](?:[a-zA-Z0-9_-]*[a-zA-Z0-9])?$`) severityName = []string{"UNKNOWN", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL", "FATAL"} + defaultRender= TreeWriter(WriteTreeFnc(CTreeWriter)) + // Global sentinels (no module prefix) ErrNilError = Code(RootModule + ModuleSep + "nil-error") @@ -89,6 +93,28 @@ var ( func isValidSegment(code string) bool { return len(code) > 0 && codeRgx.MatchString(code) } + + +func SetDefaultTreeWriter (treeWriter TreeWriter) { + rw.RLock() + if treeWriter == nil { + defaultRender = WriteTreeFnc(CTreeWriter) + } else { + defaultRender = treeWriter + } + rw.RUnlock() +} + +func GetDefaultTreeWriter () TreeWriter { + rw.RLock() + wt := defaultRender + rw.RUnlock() + return wt +} + + + + func ToError(err any) error { if err == nil { return nil diff --git a/src/core/errs/extractors.go b/src/core/errs/extractors.go index 9df1c6f..c5bd459 100644 --- a/src/core/errs/extractors.go +++ b/src/core/errs/extractors.go @@ -49,30 +49,61 @@ package errs -import "sync" +import ( + "reflect" + "sync" +) type Extractors struct { rw sync.RWMutex - extractors []Extractor + byType map[reflect.Type]ExtractFnc + interfaces []Extractor } -func NewExtractors() *Extractors { return &Extractors{} } +func NewExtractors() *Extractors { + return &Extractors{} +} + +func (e *Extractors) RegisterByType (err error, fn ExtractFnc) { + if err == nil || fn == nil { + return + } + tp := reflect.TypeOf(err) + e.rw.Lock() + if e.byType == nil { + e.byType = make(map[reflect.Type]ExtractFnc) + } + e.byType[tp] = fn + defer e.rw.Unlock() +} -func (e *Extractors) AddExtractors(extractors ... Extractor) { - if len(extractors) == 0 { return } - e.rw.Lock(); defer e.rw.Unlock() - for _,x := range extractors { - if x != nil { e.extractors = append(e.extractors, x) } +func (e *Extractors) RegisterInterface(fnc ExtractFnc) { + if fnc == nil { + return } + e.rw.Lock() + e.interfaces = append(e.interfaces, fnc) + e.rw.Unlock() } + func (e *Extractors) Extract (err error, collector Collector) { + rf := reflect.TypeOf(err) + e.rw.RLock() - defer e.rw.RUnlock() - for _,x := range e.extractors { x.Extract(err,collector) } + if ex,ok := e.byType[rf]; ok { + e.rw.Unlock() + ex(err,collector) + return + } + + for _,x := range e.interfaces { + x.Extract(err,collector) + } + e.rw.RUnlock() } diff --git a/src/core/errs/tree.go b/src/core/errs/tree.go index 85d3f28..969d7cc 100644 --- a/src/core/errs/tree.go +++ b/src/core/errs/tree.go @@ -51,7 +51,6 @@ package errs import ( "core/opts" - "fmt" "io" "strings" ) @@ -98,106 +97,30 @@ func (t *Tree) Walk (walker TreeWalkFnc) { -// shortenPath acorta rutas largas para mejor legibilidad -func shortenPath(path string) string { - parts := strings.Split(path, "/") - if len(parts) > 3 { - return ".../" + strings.Join(parts[len(parts)-2:], "/") - } - return path -} -func (t *Tree) Print(w io.Writer) { +func (t *Tree) Write(w io.Writer, treeWriter TreeWriter) error { if t == nil || t.root == nil { - return + return nil } - _ = RenderTree(t,w) + if treeWriter != nil { + return treeWriter.Write(t,w) + } + return GetDefaultTreeWriter().Write(t,w) } + // String implementa fmt.Stringer para print directo -func (t *Tree) String() string { +func (t *Tree) String(treeWriter TreeWriter) string { if t == nil || t.root == nil { return "" } - var buf strings.Builder - // Reutilizamos el renderizador pasando el builder de strings - _ = RenderTree(t, &buf) - + _ = t.Write(&buf, treeWriter) return buf.String() } -// RenderTree imprime la jerarquía de errores de manera visual y semántica. -// Iconos: -// ❌ Error -// ℹ️ Meta / atributo -// ⚠️ Stack -// Conectores para causas: -// ├─▶ (child) -// └─▶ (last) -func RenderTree(t *Tree, w io.Writer) error { - var walk func(n *TreeNode, indent string, indent_width int, hasSibling bool, isRoot bool) error - - walk = func(n *TreeNode, indent string, indent_width int, hasSibling bool, isRoot bool) error { - // --- imprimir nodo --- - var pre_width int - if isRoot { - fmt.Fprintf(w, "❌ %v\n", n.Err) - pre_width = 3 // width of "❌ " (2 for emoji + 1 for space) - } else { - conn := "├─▶ " - conn_width := 4 // width of "├─▶ " or "└─▶ " - if !hasSibling { - conn = "└─▶ " - } - fmt.Fprintf(w, "%s%s❌ %v\n", indent, conn, n.Err) - pre_width = indent_width + conn_width + 3 // +3 for "❌ " - } - - attrIndent := strings.Repeat(" ", pre_width) - - // --- imprimir metas --- - for _, meta := range n.Metas { - fmt.Fprintf(w, "%sℹ️ %s = %v\n", attrIndent, meta.Name(), meta.Value()) - } - - // --- imprimir stack --- - if len(n.Stack) > 0 { - frames := n.Stack.Frames() - fmt.Fprintf(w, "%s⚠️ stack (%d frames)\n", attrIndent, len(frames)) - for _, frame := range frames { - fmt.Fprintf(w, "%s %s:%d %s\n", attrIndent, shortenPath(frame.File), frame.LineNumber, frame.FuncName) - } - } - - // --- recorrer hijos --- - for i, c := range n.Causes { - last := i == len(n.Causes)-1 - // calcular indent para hijos - childIndent := indent - childWidth := indent_width - if !isRoot { - if hasSibling { - childIndent += "│ " - childWidth += 4 - } else { - childIndent += " " - childWidth += 4 - } - } - if err := walk(c, childIndent, childWidth, !last, false); err != nil { - return err - } - } - - return nil - } - - return walk(t.root, "", 0, false, true) -} - // WithMaxDepth establece el límite máximo de profundidad para causas en el árbol. diff --git a/src/core/errs/tree_node.go b/src/core/errs/tree_node.go index 5d2f8e2..d965727 100644 --- a/src/core/errs/tree_node.go +++ b/src/core/errs/tree_node.go @@ -51,7 +51,6 @@ package errs import ( "errors" - "fmt" ) type TreeNode struct { @@ -118,7 +117,6 @@ func (n *TreeNode) AddCauses(causes ...error) { } func (n *TreeNode) AddStack(stack Stack) { - fmt.Println("CXaptturando stack %d level %d\n",n.tree.stackFirst,n.tree.stackFirst) // 1. VALIDACIÓN GLOBAL (Nivel 0 = OFF) // 2. VALIDACIÓN VERTICAL (Profundidad) // Si NO es infinito (-1) Y el nivel actual supera o iguala el límite. diff --git a/src/core/errs/tree_writer.go b/src/core/errs/tree_writer.go new file mode 100644 index 0000000..6f0505b --- /dev/null +++ b/src/core/errs/tree_writer.go @@ -0,0 +1,199 @@ +/* —————————————————————————————————————————————————————————————————————————— + Proyecto FWS +————————————————————————————————————————————————————————————————————————————— + Fichero tree_writer.go + Package errs + Autor Juan V. Navarro juanvnl@activething.com + Creado 25/01/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/FWS + git : g.activething.com | github.com/activething/FWS + 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 ( + "bufio" + "fmt" + "io" + "strconv" + "strings" +) + + +type ( + TreeWriter interface { + Write (t *Tree, w io.Writer) error + } + + WriteTreeFnc func (t *Tree, w io.Writer) error +) + + + +func (f WriteTreeFnc) Write(t *Tree, w io.Writer) error { return f(t, w) } + + + + + + + + + + + +// Optimizamos shortenPath para evitar split/join y asignaciones innecesarias. +func shortenPathOptimized(path string) string { + last := strings.LastIndexByte(path, '/') + if last == -1 { + return path + } + // Buscamos el penúltimo separador + secondLast := strings.LastIndexByte(path[:last], '/') + if secondLast == -1 { + return path + } + // Devolvemos una rodaja de la cadena original precedida por .../ + return ".../" + path[secondLast+1:] +} + +// CTreeWriter imprime la jerarquía de errores de manera visual y semántica. +// Iconos: +// ❌ Error +// ℹ️ Meta / atributo +// ⚠️ Stack +// Conectores para causas: +// ├─▶ (child) +// └─▶ (last) +func CTreeWriter(t *Tree, w io.Writer) error { + if t == nil || t.root == nil { + return nil + } + + // Usamos bufio para agrupar escrituras pequeñas y evitar syscalls constantes + bw := bufio.NewWriter(w) + defer bw.Flush() + + // Buffer para gestionar la indentación sin crear constantes strings + // "│ " o " " ocupan 4 bytes por nivel. 32 niveles = 128 bytes. + indentBuf := make([]byte, 0, 128) + + var walk func(n *TreeNode, hasSibling bool, isRoot bool) error + + walk = func(n *TreeNode, hasSibling bool, isRoot bool) error { + // 1. DIBUJAR NODO + if isRoot { + bw.WriteString("❌ ") + bw.WriteString(n.Err.Error()) + bw.WriteByte('\n') + } else { + bw.Write(indentBuf) + if hasSibling { + bw.WriteString("├─▶ ❌ ") + } else { + bw.WriteString("└─▶ ❌ ") + } + bw.WriteString(n.Err.Error()) + bw.WriteByte('\n') + } + + // 2. CALCULAR PREFIJO PARA ATRIBUTOS (Metas y Stacks) + // El prefijo depende de si el nodo actual tiene hermanos o no + oldLen := len(indentBuf) + if !isRoot { + if hasSibling { + indentBuf = append(indentBuf, "│ "...) + } else { + indentBuf = append(indentBuf, " "...) + } + } + + // Prefijo base para metas: indentación actual + espacio del icono + attrPrefix := " " // Espacio equivalente al conector "├─▶ " + + // 3. RENDERIZAR METAS + for _, m := range n.Metas { + bw.Write(indentBuf) + bw.WriteString(attrPrefix) + bw.WriteString("ℹ️ ") + bw.WriteString(m.Name()) + bw.WriteString(" = ") + fmt.Fprint(bw, m.Value()) // Solo usamos fmt para valores desconocidos + bw.WriteByte('\n') + } + + // 4. RENDERIZAR STACK + if len(n.Stack) > 0 { + frames := n.Stack.Frames() + bw.Write(indentBuf) + bw.WriteString(attrPrefix) + bw.WriteString("⚠️ stack (") + bw.WriteString(strconv.Itoa(len(frames))) + bw.WriteString(" frames)\n") + + for _, f := range frames { + bw.Write(indentBuf) + bw.WriteString(attrPrefix) + bw.WriteString(" ") + bw.WriteString(shortenPathOptimized(f.File)) + bw.WriteByte(':') + bw.WriteString(strconv.Itoa(f.LineNumber)) + bw.WriteByte(' ') + bw.WriteString(f.FuncName) + bw.WriteByte('\n') + } + } + + // 5. RECURSIÓN PARA CAUSAS + for i, c := range n.Causes { + isLast := i == len(n.Causes)-1 + if err := walk(c, !isLast, false); err != nil { + return err + } + } + + // Restaurar el buffer de indentación al volver de la recursión + indentBuf = indentBuf[:oldLen] + return nil + } + + return walk(t.root, false, true) +} \ No newline at end of file