diff --git a/cmd/zen/serve/serve.go b/cmd/zen/serve/serve.go index 3659286..e5e2d85 100644 --- a/cmd/zen/serve/serve.go +++ b/cmd/zen/serve/serve.go @@ -10,6 +10,8 @@ import ( "golang.org/x/net/http2" "golang.org/x/net/http2/h2c" + + _ "gitlab.bareksa.com/backend/zen/core/zerr" ) func Serve() { diff --git a/core/zerr/err.go b/core/zerr/err.go index ab065cc..b3802f5 100644 --- a/core/zerr/err.go +++ b/core/zerr/err.go @@ -3,12 +3,14 @@ package zerr import ( "context" "fmt" + "io" "log/slog" "strconv" "strings" "time" "connectrpc.com/connect" + "github.com/pborman/indent" "gitlab.bareksa.com/backend/zen/core/zoptions" ) @@ -28,73 +30,81 @@ type Err struct { sequence *Sequence } +// Join implements Error. +func (mu *Err) Join(errs ...error) Error { + for _, err := range errs { + if err != nil { + mu.errs = append(mu.errs, err) + } + } + return mu +} + +// Resolve implements Error. +func (mu *Err) Resolve() error { + if len(mu.errs) == 0 { + return nil + } + return mu +} + func (mu *Err) Error() string { if len(mu.errs) == 0 { return "[nil]" } - s := strings.Builder{} - if len(mu.errs) == 1 { - str := getNestedErrorString(mu.errs[0]) - str, found := strings.CutPrefix(str, mu.message) - if found { - str, _ = strings.CutPrefix(str, ": ") - } + s := &strings.Builder{} + if mu.message != "" { s.WriteString(mu.message) + } + if len(mu.errs) == 1 { + err := mu.errs[0] + if wb, ok := err.(interface{ WriteBuilder(io.Writer) }); ok { + wb.WriteBuilder(s) + return s.String() + } + next := strings.TrimPrefix(err.Error(), mu.message) + if next == "" { + return s.String() + } s.WriteString(": ") - s.WriteString(str) + s.WriteString(next) return s.String() } - s.WriteString(mu.message) + if mu.message != "" { + s.WriteString(":\n") + } + w := indent.New(s, " ") for i, e := range mu.errs { if i > 0 { - s.WriteString("\n") + _, _ = io.WriteString(w, "\n") } - s.WriteString(strconv.Itoa(i + 1)) - s.WriteString(". ") - s.WriteString(getNestedErrorString(e)) - } - return s.String() -} - -func getNestedErrorString(err error) string { - if err == nil { - return "[nil]" - } - s := strings.Builder{} - if e, ok := err.(interface{ Unwrap() error }); ok { - current := err.Error() - if err := e.Unwrap(); err != nil { - next := getNestedErrorString(err) - hasPrefix := strings.HasPrefix(next, current) - if !hasPrefix { - s.WriteString(current) - s.WriteString(": ") - } - s.WriteString(next) + _, _ = io.WriteString(w, strconv.Itoa(i+1)) + _, _ = io.WriteString(w, ". ") + if wb, ok := e.(interface{ WriteBuilder(io.Writer) }); ok { + wb.WriteBuilder(w) + } else { + _, _ = io.WriteString(w, e.Error()) } } - - if errs, ok := err.(interface{ Unwrap() []error }); ok { - for i, e := range errs.Unwrap() { - if i > 0 { - s.WriteString("\n") - } - s.WriteString(strconv.Itoa(i + 1)) - s.WriteString(". ") - s.WriteString(getNestedErrorString(e)) - } - } - - if s.Len() == 0 { - s.WriteString(err.Error()) - } - return s.String() } // LogValue returns log fields to be consumed by logger. func (mu *Err) LogValue() slog.Value { - attrs := make([]slog.Attr, 0, len(mu.errs)) + attrs := make([]slog.Attr, 0, 8) + if len(mu.message) > 0 { + attrs = append(attrs, slog.String("message", mu.message)) + } + if len(mu.publicMessage) > 0 { + attrs = append(attrs, slog.String("public", mu.message)) + } + if mu.code != connect.CodeUnknown && mu.code != connect.CodeInternal { + attrs = append(attrs, slog.String("code", mu.code.String())) + } + if mu.sequence.IsRoot() { + } + if len(mu.errs) == 1 { + } i := 0 for _, err := range mu.errs { if err == nil { diff --git a/core/zerr/err_write_to.go b/core/zerr/err_write_to.go index 01c98d4..f190050 100644 --- a/core/zerr/err_write_to.go +++ b/core/zerr/err_write_to.go @@ -11,7 +11,7 @@ import ( // WriteBuilder writes the error to the builder. // // w is guaranteed to never return error because -// the underlying most writer is a strings.Builder. +// the bottom most writer is a strings.Builder. func (er *Err) WriteBuilder(w io.Writer) { if len(er.errs) == 0 { _, _ = io.WriteString(w, "[nil]") @@ -21,7 +21,7 @@ func (er *Err) WriteBuilder(w io.Writer) { if err := er.sequence.Outer(); err != nil && !err.IsMulti() { previous = err.GetMessage() } - current, _ := strings.CutPrefix(er.message, previous) + current := strings.TrimPrefix(er.message, previous) if current != "" { if previous != "" { _, _ = io.WriteString(w, ": ") diff --git a/core/zerr/error.go b/core/zerr/error.go index 513f480..88c75f0 100644 --- a/core/zerr/error.go +++ b/core/zerr/error.go @@ -52,10 +52,24 @@ type Error interface { // // ID is used to identify the error. Used by Zen to consider if an error is the same as another. // - // Current implementation - ID(msg string, args ...any) Error + // If unset, the ID will be automatically generated depending + // on the backend logging and notification. + ID(s string, args ...any) Error GetID() string + // Join appends given errors to this zerr.Error. + // + // Join discards nil errors. + // + // When searching for Code, Message, PublicMessage, etc, + // only the first error containing valid values will be used. + // + // The searching method is depth-first. + // + // It's undefined behavior to call Join when another thread calls + // .Sequence().Iter() on this error. + Join(errs ...error) Error + Log(ctx context.Context) Error Notify(ctx context.Context, opts ...zoptions.NotifyOption) Error @@ -66,6 +80,7 @@ type Error interface { IsMulti() bool // Resolve returns error (self) if all the errors - // are valid. + // are valid (not nil) and Error contains at least one error + // wrapped. Resolve() error } diff --git a/core/zerr/sequence.go b/core/zerr/sequence.go index d6d4c38..4b93e44 100644 --- a/core/zerr/sequence.go +++ b/core/zerr/sequence.go @@ -2,97 +2,79 @@ package zerr import "iter" -// Sequence is the implementation of zerr.Sequence. +// Sequence keeps track of the sequence of errors. // // The implementation is reversed linked list. // -// New Sequence value will be added to the start of the list. +// When wrapping errors, the wrapping error +// will be the parent of the wrapped Error +// if the wrapped Error is a zerr.Error. type Sequence struct { - prev *Sequence - next *Sequence - err Error - index int + parent *Sequence + children []*Sequence + err Error } -// Set prepends the next Sequence to the current Sequence. -func (se *Sequence) Set(next *Sequence) { - next.next = se - se.prev = next - se.index = next.index + 1 - if next.next != nil { - next.next.Set(se) - } +// WrapBy prepends the next Sequence to the current Sequence. +func (se *Sequence) WrapBy(parent *Sequence) { + parent.children = append(parent.children, se) + se.parent = parent } // Outer returns the error that wraps this Error. // Return nil if this Error is not wrapped // or wrapping outer is not a zerr.Error. func (se *Sequence) Outer() Error { - if se.prev != nil { - return se.prev.err + if se.parent != nil { + return se.parent.err } return nil } -// Inner returns the inner zerr.Error in the sequence. -// -// if there is no inner zerr.Error, it returns nil. -// -// if the inner error is not a zerr.Error, this returns nil. -func (se *Sequence) Inner() Error { - if se.next != nil { - return se.next.err - } - return nil -} - -// Index returns the index of this Error in the sequence. -func (se *Sequence) Index() int { - return se.index -} - // Root returns the outermost Error in the sequence. // // Returns self if there is no outermost. func (se *Sequence) Root() *Sequence { root := se - for root.prev != nil { - root = root.prev + for root.parent != nil { + root = root.parent } return root } +// IsRoot checks if the Error is the outermost Error in the sequence. func (se *Sequence) IsRoot() bool { - return se.prev == nil + return se.parent == nil +} + +// Error returns the Error in the sequence. +func (se *Sequence) Error() Error { + return se.err } // Iter returns an iterator to traverse the sequence. // -// The iterator will start from outermost zerr.Error (root) to the last zerr.Error -// in depth-first order. -// // Iterator will skip any nil values in the sequence. // -// Iterator does not guarantee idempotent behavior. -// Next call to iterator may start from different root -// because the parent in the tree may be wrapped -// by another zerr.Error. +// Iteration is traversed depth first. // // Example: // -// for _, e := range err.Sequence().Iter() { +// for e := range err.Sequence().Iter() { // // e is zerr.Error // // do something with e // } func (se *Sequence) Iter() iter.Seq[Error] { - root := se.Root() - return iter.Seq[Error](func(yield func(V Error) bool) { - for next := root; next != nil; next = next.next { - if next.err != nil { - if !yield(next.err) { + return func(yield func(Error) bool) { + if se.err != nil && !yield(se.err) { + return + } + for _, child := range se.children { + for e := range child.Iter() { + if !yield(e) { return } } } - }) + } }