src.elv.sh@v0.21.0-dev.0.20240515223629-06979efb9a2a/pkg/diag/error.go (about) 1 package diag 2 3 import ( 4 "fmt" 5 "strings" 6 7 "src.elv.sh/pkg/strutil" 8 ) 9 10 // Error represents an error with context that can be showed. 11 type Error[T ErrorTag] struct { 12 Message string 13 Context Context 14 } 15 16 // ErrorTag is used to parameterize [Error] into different concrete types. The 17 // ErrorTag method is called with a zero receiver, and its return value is used 18 // in [Error.Error] and [Error.Show]. 19 type ErrorTag interface { 20 ErrorTag() string 21 } 22 23 // RangeError combines error with [Ranger]. 24 type RangeError interface { 25 error 26 Ranger 27 } 28 29 // Error returns a plain text representation of the error. 30 func (e *Error[T]) Error() string { 31 return errorTag[T]() + ": " + e.errorNoType() 32 } 33 34 func (e *Error[T]) errorNoType() string { 35 return e.Context.describeRange() + ": " + e.Message 36 } 37 38 // Range returns the range of the error. 39 func (e *Error[T]) Range() Ranging { 40 return e.Context.Range() 41 } 42 43 var ( 44 messageStart = "\033[31;1m" 45 messageEnd = "\033[m" 46 ) 47 48 // Show shows the error. 49 func (e *Error[T]) Show(indent string) string { 50 return errorTagTitle[T]() + ": " + e.showNoType(indent) 51 } 52 53 func (e *Error[T]) showNoType(indent string) string { 54 indent += " " 55 return messageStart + e.Message + messageEnd + 56 "\n" + indent + e.Context.Show(indent) 57 } 58 59 // PackErrors packs multiple instances of [Error] with the same tag into one 60 // error: 61 // 62 // - If called with no errors, it returns nil. 63 // 64 // - If called with one error, it returns that error itself. 65 // 66 // - If called with more than one [Error], it returns an error that combines 67 // all of them. The returned error also implements [Shower], and its Error 68 // and Show methods only print the tag once. 69 func PackErrors[T ErrorTag](errs []*Error[T]) error { 70 switch len(errs) { 71 case 0: 72 return nil 73 case 1: 74 return errs[0] 75 default: 76 return append(multiError[T](nil), errs...) 77 } 78 } 79 80 // UnpackErrors returns the constituent [Error] instances in an error if it is 81 // built from [PackErrors]. Otherwise it returns nil. 82 func UnpackErrors[T ErrorTag](err error) []*Error[T] { 83 switch err := err.(type) { 84 case *Error[T]: 85 return []*Error[T]{err} 86 case multiError[T]: 87 return append([]*Error[T](nil), err...) 88 default: 89 return nil 90 } 91 } 92 93 type multiError[T ErrorTag] []*Error[T] 94 95 func (err multiError[T]) Error() string { 96 var sb strings.Builder 97 fmt.Fprintf(&sb, "multiple %s: ", errorTagPlural[T]()) 98 for i, e := range err { 99 if i > 0 { 100 sb.WriteString("; ") 101 } 102 sb.WriteString(e.errorNoType()) 103 } 104 return sb.String() 105 } 106 107 func (err multiError[T]) Show(indent string) string { 108 var sb strings.Builder 109 fmt.Fprintf(&sb, "Multiple %s:", errorTagPlural[T]()) 110 indent += " " 111 for _, e := range err { 112 sb.WriteString("\n" + indent) 113 sb.WriteString(e.showNoType(indent)) 114 } 115 return sb.String() 116 } 117 118 func errorTag[T ErrorTag]() string { 119 var t T 120 return t.ErrorTag() 121 } 122 123 // We don't have any error tags with an irregular plural yet. When we do, we can 124 // let ErrorTag optionally implement interface{ ErrorTagPlural() } and use that 125 // when available. 126 func errorTagPlural[T ErrorTag]() string { return errorTag[T]() + "s" } 127 128 func errorTagTitle[T ErrorTag]() string { return strutil.Title(errorTag[T]()) }